agentp 0.9.0 → 0.11.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/lib/opencode.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const http = require('http');
4
+ const https = require('https');
4
5
  const { URL } = require('url');
5
6
 
6
7
  function getAuthHeaders() {
@@ -11,7 +12,7 @@ function getAuthHeaders() {
11
12
  return { 'Authorization': `Basic ${encoded}` };
12
13
  }
13
14
 
14
- function makeRequest(options, data, cancelRef) {
15
+ function makeRequest(options, data, cancelRef, timeoutMs) {
15
16
  return new Promise((resolve, reject) => {
16
17
  const req = http.request(options, (res) => {
17
18
  let body = '';
@@ -24,6 +25,12 @@ function makeRequest(options, data, cancelRef) {
24
25
  }
25
26
  });
26
27
  });
28
+ if (timeoutMs) {
29
+ req.setTimeout(timeoutMs, () => {
30
+ req.destroy();
31
+ reject(new Error('Request timed out'));
32
+ });
33
+ }
27
34
  req.on('error', reject);
28
35
  if (cancelRef) cancelRef.current = req;
29
36
  if (data) req.write(data);
@@ -243,6 +250,36 @@ async function selectSession(server, sessionId) {
243
250
  }
244
251
  }
245
252
 
253
+ // Get a single session by ID, including its message history.
254
+ // Tries GET /session/:id first; falls back to filtering the sessions list.
255
+ async function getSession(server, sessionId) {
256
+ const encoded = encodeURIComponent(sessionId);
257
+ // Try several endpoint patterns that different opencode versions may expose
258
+ const urls = [
259
+ `${server}/session/${encoded}`,
260
+ `${server}/session/${encoded}/messages`,
261
+ `${server}/session/${encoded}/history`,
262
+ `${server}/session/${encoded}/conversation`,
263
+ `${server}/conversation/${encoded}`,
264
+ ];
265
+ for (const url of urls) {
266
+ try {
267
+ const { status, body } = await makeRequest(buildJsonRequest(url, 'GET'), null, null, 10000);
268
+ if (status === 200) {
269
+ try {
270
+ const parsed = JSON.parse(body);
271
+ if (parsed != null) return parsed;
272
+ } catch {}
273
+ }
274
+ } catch {}
275
+ }
276
+ // Fallback: get all sessions and find the one we need
277
+ const sessions = await listSessions(server);
278
+ const match = sessions.find(s => String(s.id) === String(sessionId));
279
+ if (match) return match;
280
+ throw new Error(`Session ${sessionId} not found`);
281
+ }
282
+
246
283
  // List all agents from the OpenCode server.
247
284
  async function listAgents(server) {
248
285
  const url = `${server}/agent`;
@@ -283,8 +320,13 @@ async function respondToPermission(server, sessionId, permissionId, response, re
283
320
  // permission requests. Resolves with the full collected text when the session goes idle.
284
321
  // callbacks: { onText(chunk), onPermission(permission), onThinking(chunk), onConnected() }
285
322
  // cancelRef — allows aborting the SSE stream via req.destroy()
286
- function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
287
- process.stderr.write(` [SSE] connecting to ${server}/event for session ${sessionId}\n`);
323
+ // logFn — optional logger; if provided, used instead of process.stderr.write
324
+ function listenForSessionEvents(server, sessionId, callbacks, cancelRef, logFn) {
325
+ const sseLog = logFn || ((msg) => {
326
+ const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
327
+ process.stderr.write(`${ts} ${msg}`);
328
+ });
329
+ sseLog(` [SSE] connecting to ${server}/event for session ${sessionId}\n`);
288
330
  return new Promise((resolve, reject) => {
289
331
  const url = `${server}/event`;
290
332
  const parsed = new URL(url);
@@ -301,7 +343,7 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
301
343
  reject(new Error('OpenCode server authentication failed. Set OPENCODE_SERVER_PASSWORD to match the server password.'));
302
344
  return;
303
345
  }
304
- process.stderr.write(` [SSE] connected for session ${sessionId}\n`);
346
+ sseLog(` [SSE] connected for session ${sessionId}\n`);
305
347
  if (callbacks.onConnected) callbacks.onConnected();
306
348
  let buffer = '';
307
349
  const userMessageIDs = new Set();
@@ -312,7 +354,7 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
312
354
  const safetyTimer = setTimeout(() => {
313
355
  if (resolved) return;
314
356
  resolved = true;
315
- process.stderr.write(` [SSE] safety timeout (90s) for session ${sessionId}, collected ${collected.length} chars\n`);
357
+ sseLog(` [SSE] safety timeout (90s) for session ${sessionId}, collected ${collected.length} chars\n`);
316
358
  cleanup();
317
359
  req.destroy();
318
360
  res.destroy();
@@ -338,9 +380,9 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
338
380
  const props = event.properties || {};
339
381
 
340
382
  if (event.type === 'session.status') {
341
- process.stderr.write(` [SSE] session.status: type=${props.status?.type} status=${JSON.stringify(props.status)}\n`);
383
+ sseLog(` [SSE] session.status: type=${props.status?.type} status=${JSON.stringify(props.status)}\n`);
342
384
  } else {
343
- process.stderr.write(` [SSE] event type=${event.type} sessionID=${props.sessionID} waitSession=${sessionId}\n`);
385
+ sseLog(` [SSE] event type=${event.type} sessionID=${props.sessionID} waitSession=${sessionId}\n`);
344
386
  }
345
387
 
346
388
  // Filter by sessionID when the event carries one (string-compare to handle type mismatches)
@@ -353,18 +395,18 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
353
395
 
354
396
  if (event.type === 'permission.asked') {
355
397
  const permStr = JSON.stringify(props);
356
- process.stderr.write(` [SSE] PERMISSION ASKED: ${permStr}\n`);
398
+ sseLog(` [SSE] PERMISSION ASKED: ${permStr}\n`);
357
399
  if (callbacks.onPermission) {
358
- process.stderr.write(` [SSE] calling onPermission callback\n`);
400
+ sseLog(` [SSE] calling onPermission callback\n`);
359
401
  callbacks.onPermission(props);
360
402
  } else {
361
- process.stderr.write(` [SSE] WARNING: no onPermission callback registered\n`);
403
+ sseLog(` [SSE] WARNING: no onPermission callback registered\n`);
362
404
  }
363
405
  continue;
364
406
  }
365
407
 
366
408
  if (event.type === 'permission.replied') {
367
- process.stderr.write(` [SSE] PERMISSION REPLIED: type=${props.type}\n`);
409
+ sseLog(` [SSE] PERMISSION REPLIED: type=${props.type}\n`);
368
410
  if (callbacks.onPermissionReplied) {
369
411
  callbacks.onPermissionReplied(props);
370
412
  }
@@ -426,7 +468,7 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
426
468
  if (resolved) return;
427
469
  resolved = true;
428
470
  cleanup();
429
- process.stderr.write(` [SSE] response stream error for session ${sessionId}: ${err.message}\n`);
471
+ sseLog(` [SSE] response stream error for session ${sessionId}: ${err.message}\n`);
430
472
  resolve(collected); // resolve with what we have
431
473
  });
432
474
  });
@@ -445,6 +487,29 @@ function listenForSessionEvents(server, sessionId, callbacks, cancelRef) {
445
487
  });
446
488
  }
447
489
 
490
+ // Check if an OpenCode server is reachable by making a GET /session request.
491
+ // Returns a Promise that resolves to `true` (any response) or `false` (connection error / timeout).
492
+ function isServerAlive(url) {
493
+ return new Promise((resolve) => {
494
+ const mod = url.startsWith('https') ? https : http;
495
+ const parsed = new URL(url + '/session');
496
+ const opts = {
497
+ hostname: parsed.hostname,
498
+ port: parsed.port,
499
+ path: parsed.pathname,
500
+ method: 'GET',
501
+ headers: { ...getAuthHeaders() },
502
+ };
503
+ const req = mod.request(opts, (res) => {
504
+ res.resume();
505
+ resolve(true);
506
+ });
507
+ req.on('error', () => resolve(false));
508
+ req.setTimeout(5000, () => { req.destroy(); resolve(false); });
509
+ req.end();
510
+ });
511
+ }
512
+
448
513
  module.exports = {
449
514
  getAuthHeaders,
450
515
  makeRequest,
@@ -455,6 +520,7 @@ module.exports = {
455
520
  sendText,
456
521
  listenForFinalAnswer,
457
522
  listSessions,
523
+ getSession,
458
524
  createSession,
459
525
  updateSession,
460
526
  selectSession,
@@ -464,4 +530,5 @@ module.exports = {
464
530
  listenForSessionEvents,
465
531
  listAgents,
466
532
  listProviders,
533
+ isServerAlive,
467
534
  };
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "agentp",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
5
5
  "bin": {
6
6
  "agentp": "bin/agentp",
7
7
  "ocmux": "bin/ocmux",
8
8
  "tgagentp": "bin/tgagentp"
9
9
  },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "README.md",
14
+ "CONTRIBUTING.md",
15
+ "docs/"
16
+ ],
10
17
  "keywords": [
11
18
  "ai",
12
19
  "cli",
@@ -24,7 +31,7 @@
24
31
  "node": ">=18"
25
32
  },
26
33
  "scripts": {
27
- "test": "echo \"Error: no test specified\" && exit 1"
34
+ "test": "node --test tests/*.test.js"
28
35
  },
29
36
  "type": "commonjs"
30
37
  }
package/AGENTS.md DELETED
@@ -1,177 +0,0 @@
1
- # agentp — agent notes
2
-
3
- ## Three binaries, all registered
4
-
5
- - `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode session
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
8
-
9
- ## Package facts
10
-
11
- - **Zero npm dependencies.** All scripts use only Node.js stdlib (`http`, `https`, `readline`, `url`, `child_process`, `fs`, `path`, `crypto`, `os`).
12
- - **No tests, no CI, no lint, no formatter.** `npm test` is a stub that exits 1.
13
- - **No lockfile.** Versions resolve at install time.
14
- - **`"type": "commonjs"`**, requires Node >= 18.
15
- - **Linux and macOS compatible.** All tools use portable Node.js APIs only.
16
-
17
- ## Logging conventions (tgagentp)
18
-
19
- - **stdout** — informational messages (startup, discovery, session switches)
20
- - **stderr** — errors (always shown) + trace/debug (only with `--verbose` flag)
21
- - Default usage for clean console: `tgagentp 2>/dev/null`
22
- - The `log` helper object at the top of `bin/tgagentp` provides `log.info()`, `log.error()`, and `log.debug()`.
23
-
24
- ## `.ocmux.json`
25
-
26
- Runtime state file created by `bin/ocmux` (contains URL, log path, and window index). **Not in `.gitignore`** — avoid committing it.
27
-
28
- ## Shared `lib/opencode.js`
29
-
30
- Extracted from `bin/agentp`; used by both `agentp` and `tgagentp`:
31
-
32
- - `getAuthHeaders()` — reads `OPENCODE_SERVER_PASSWORD` / `OPENCODE_SERVER_USERNAME`
33
- - `makeRequest(options, data)` — thin `http.request` wrapper returning a Promise
34
- - `buildJsonRequest(url, method, body)` — builds request options for JSON POST/GET (no Content-Type/Length for bodyless requests)
35
- - `clearPrompt(server)`, `appendPrompt(server, text)`, `submitPrompt(server)` — TUI prompt endpoints
36
- - `sendText(server, text)` — convenience: clear + append + submit in one call
37
- - `listenForFinalAnswer(server, onText?, cancelRef?)` — SSE event stream listener; `cancelRef` allows true server-side abort via `req.destroy()`
38
- - `listSessions(server, directory?)` — `GET /session`; returns parsed JSON array; optional `?directory=` filter
39
- - `createSession(server, title?)` — `POST /session`; returns the created `Session` object; optional `title`
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`
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)
45
- - `listAgents(server)` — `GET /agent`; returns parsed JSON array of agent objects
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)
48
-
49
- ## Shared `lib/ocmux.js`
50
-
51
- Shared ocmux session discovery; used by both `bin/ocmux` and `bin/tgagentp`:
52
-
53
- - `readState(file)` — parses `.ocmux.json`, returns `null` on error
54
- - `statefileFor(dir)` — `path.join(dir, '.ocmux.json')`
55
- - `tuiPaneId(windowIndex)` — returns pane ID of the TUI pane (pane index != 0), `null` if dead
56
- - `windowByDir(dir)` — returns window index by name match
57
- - `windowNameByIndex(idx)` — reverse: name from index
58
- - `activeWindowIndex()` — returns index of currently selected tmux window
59
- - `paneCount(windowIndex)` — number of panes in a window
60
- - `listServers()` — scans all tmux windows for `.ocmux.json` state files; returns `{ url, dir, index, status }` array
61
- - `activateServer(dir, index, url)` — pin window name, restart dead TUI pane, select window, zoom pane
62
-
63
- ## OpenCode HTTP protocol
64
-
65
- These endpoints are consumed but not documented elsewhere in the repo:
66
-
67
- - `POST /tui/clear-prompt`
68
- - `POST /tui/append-prompt`
69
- - `POST /tui/submit-prompt`
70
- - `GET /event` (SSE stream)
71
- - `GET /session` — list sessions (optional `?directory=` filter)
72
- - `POST /session` — create a new session (optional `{ title? }` body)
73
- - `PATCH /session/:id` — update session properties (`{ title }`, optional `{ agent }`)
74
- - `POST /session/:id/select` — tell the TUI to navigate to a session (only with TUI attached)
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)
77
- - `GET /agent` — list available agents
78
- - `GET /provider` — list connected providers/models
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
-
90
- ## OpenCode tmux session model (ocmux)
91
-
92
- - Single tmux session named `"Opencode"`
93
- - Windows named by full project directory path
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
95
- - Log file per project: `/tmp/opencode-serve-<hashDir(dir)>.log` — captured via `tee`, read by ocmux polling loop
96
- - Per-project `.ocmux.json` with upward directory search (git-like)
97
- - Auto-restarts dead TUI panes on switch (respawn-pane or split-window)
98
- - Auto-zooms TUI pane on switch/create
99
-
100
- ## Commands
101
-
102
- | Command | What |
103
- |---|---|---|
104
- | `npm link` | local dev install |
105
- | `npm install -g .` | alternative local install |
106
- | `agentp [--qa] [--tg|--no-tg] [port]` | pipe stdin → opencode session, stream answer to stdout (uses session API); `--tg` forwards answer to Telegram |
107
- | `tgagentp [port]` | bridge Telegram bot ↔ opencode TUI (needs `TELEGRAM_BOT_TOKEN`) |
108
- | `tgagentp --dev` | enable `/shutdown` command for remote restart |
109
- | `ocmux serve [--print-logs] [dir]` | start opencode serve in a tmux window (primary verb) |
110
- | `ocmux new [dir]` | alias for `serve` (backwards compat, to be removed in 1.0) |
111
- | `ocmux kill [dir]` | kill server + tmux window |
112
- | `ocmux list [-l]` | list running servers |
113
- | `ocmux [dir]` | switch to existing opencode tmux window |
114
-
115
- ## TODO
116
-
117
- ### Pending
118
-
119
- - `agentp --getLast n` — retrieve last n answers from session history
120
-
121
- ### In Progress
122
-
123
- *(none)*
124
-
125
- ### Done
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
136
- - Logging rebalance: stdout for info, stderr for errors/debug/trace, `--verbose` flag, `2>/dev/null` default usage
137
- - `/shutdown force` — refuses shutdown when busy unless `force` flag is given; goodbye message includes state
138
- - `/think [on|off|switch]` — toggle real-time forwarding of model thinking messages to the chat; `--think` CLI flag to start enabled
139
- - Event-driven permission handling (`POST /session/:id/prompt_async` + SSE listener + `/allow`, `/reject`, `/always` commands) — included in v0.7.0
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)
141
- - /sessions new [name] — create a new session, TUI follows via /session/:id/select
142
- - /sessions rename <name> — rename the current active session
143
- - Merge /session into /sessions (keep /session as alias)
144
- - Case-insensitive matching for all switch subcommands (/servers, /sessions, /agents)
145
- - Fix: /sessions filter includes sessions with directory (TUI sessions) + active session exception
146
- - Fix: /sessions now shows the active session even without agent set (exception in filter)
147
- - Fix: /sessions switch and /sessions new call selectSession (TUI navigation didn't work in opencode < 1.15.x)
148
- - Fix: active session highlighted with ▶ bullet in session list
149
- - Fix: discover TUI session on startup and server switch via `discoverActiveSession()`
150
- - Message splitting (>4096 chars, breaks at newlines)
151
- - Fix: tuiPaneId detects 'bun' (opencode 1.15.x) in addition to 'node'
152
- - Fix: URL polling in ocmux doNew takes last match, not first (more robust with bun startup)
153
- - lib/ocmux.js shared session discovery, refactor ocmux + tgagentp
154
- - /cancel with true server-side SSE abort via cancelRef
155
- - /shutdown (--dev mode) with Telegram offset acknowledgment
156
-
157
- ### Verified
158
-
159
- - Password auth support (OPENCODE_SERVER_PASSWORD) for agentp, ocmux, tgagentp
160
- - agentp HTTP Basic Auth headers on all requests (POST and SSE)
161
- - agentp clear 401 error reporting
162
- - ocmux interactive shells (send-keys) for env var propagation
163
- - tgagentp Telegram bridge bot with long-polling
164
- - Markdown→HTML converter (**bold**, *italic*, `code`, code blocks, headings, tables)
165
- - Chat allowlist (TGAGENTP_ALLOWED_CHAT_IDS)
166
- - Slash command system (/help, /servers, /sessions, /session, /agents, /agents switch, /models, /status, /cancel, /shutdown)
167
- - Per-server state tracking (busy/idle, pendingResponse, activeSessionId, activeAgentName, taskId)
168
- - Background async processing (non-commands fire without await)
169
- - /servers switch activates tmux window (pin, restart TUI, select, zoom)
170
- - Startup greeting with /status-style info persisted via temp file
171
- - Fix port mismatch bug: truncate stale log file before send-keys in doNew
172
- - Update README with full tgagentp features (slash commands, --dev, env vars)
173
- - Update AGENTS.md with lib/ocmux.js, missing API endpoints, current architecture
174
- - Single-user lock: first chat to connect owns the session
175
- - Fix startup greeting markdown not being converted to HTML
176
-
177
- *(Keep this TODO up to date: move items between categories as their status changes.)*
package/CHANGELOG.md DELETED
@@ -1,24 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- ## [Unreleased]
6
-
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.
10
-
11
- ## [0.8.0] - 2026-06-03
12
-
13
- - Fix `/servers` crash: `serverBase is not defined` error.
14
- - Logging rebalance: stdout for info, stderr for errors/debug with `--verbose` flag (`2>/dev/null` for clean console).
15
- - `/shutdown force`: refuse shutdown when busy unless `force` flag given.
16
- - `/think [on|off|switch]`: toggle real-time forwarding of model thinking messages; `--think` CLI flag.
17
- - Reply quoting: when replying to a Telegram message, prepend quoted text with safe backtick fencing.
18
- - Reply chaining: answers use `reply_to_message_id` to appear as replies.
19
- - TUI navigation: `POST /tui/select-session` tried first (works in opencode 1.15.13).
20
-
21
- ## [0.1.0] - 2026-05-08
22
-
23
- - Initial release.
24
- - Add project documentation and usage examples.