agentp 0.1.2 → 0.3.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 +34 -24
- package/README.md +53 -9
- package/bin/agentp +12 -4
- package/bin/ocmux +604 -0
- package/package.json +3 -2
- package/.ocmux.json +0 -1
package/AGENTS.md
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
# agentp — agent notes
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Two binaries, both registered
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `bin/agentp` (Node.js) — registered in `package.json` `"bin"` — pipes stdin → opencode TUI
|
|
6
|
+
- `bin/ocmux` (Node.js) — registered in `package.json` `"bin"` — manages opencode tmux sessions
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## Package facts
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- **Zero npm dependencies.** Both scripts use only Node.js stdlib (`http`, `readline`, `url`, `child_process`, `fs`, `path`, `crypto`, `os`).
|
|
11
|
+
- **No tests, no CI, no lint, no formatter.** `npm test` is a stub that exits 1.
|
|
12
|
+
- **No lockfile.** Versions resolve at install time.
|
|
13
|
+
- **`"type": "commonjs"`**, requires Node >= 18.
|
|
14
|
+
- **Linux and macOS compatible.** Both tools use portable Node.js APIs only.
|
|
10
15
|
|
|
11
|
-
##
|
|
16
|
+
## `.ocmux.json`
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|---|---|
|
|
15
|
-
| Dev install | `npm link` or `npm install -g .` |
|
|
16
|
-
| Run | `printf "prompt" \| agentp` |
|
|
18
|
+
Runtime state file created by `bin/ocmux` (contains URL, log path, and window index). **Not in `.gitignore`** — avoid committing it.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## OpenCode HTTP protocol (agentp)
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
These endpoints are consumed but not documented elsewhere in the repo:
|
|
21
23
|
|
|
22
|
-
-
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
24
|
+
- `POST /tui/clear-prompt`
|
|
25
|
+
- `POST /tui/append-prompt`
|
|
26
|
+
- `POST /tui/submit-prompt`
|
|
27
|
+
- `GET /event` (SSE stream)
|
|
25
28
|
|
|
26
|
-
##
|
|
29
|
+
## OpenCode tmux session model (ocmux)
|
|
27
30
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
31
|
+
- Single tmux session named `"Opencode"`
|
|
32
|
+
- Windows named by full project directory path
|
|
33
|
+
- Server pane runs `opencode serve --port 0`, TUI pane runs `opencode attach --continue`
|
|
34
|
+
- Per-project `.ocmux.json` with upward directory search (git-like)
|
|
35
|
+
- Auto-restarts dead TUI panes on switch
|
|
36
|
+
- Auto-zooms TUI pane on switch/create
|
|
33
37
|
|
|
34
|
-
##
|
|
38
|
+
## Commands
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
| Command | What |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `npm link` | local dev install |
|
|
43
|
+
| `npm install -g .` | alternative local install |
|
|
44
|
+
| `agentp [--qa] [port]` | pipe stdin → opencode TUI, stream answer to stdout |
|
|
45
|
+
| `ocmux new [dir]` | start opencode serve in a tmux window |
|
|
46
|
+
| `ocmux kill [dir]` | kill server + tmux window |
|
|
47
|
+
| `ocmux list [-l]` | list running servers |
|
|
48
|
+
| `ocmux [dir]` | switch to existing opencode tmux window |
|
package/README.md
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/agentp)
|
|
6
6
|
[](https://www.npmjs.com/package/agentp)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
This package provides two CLI tools:
|
|
9
|
+
|
|
10
|
+
- **`agentp`** — pipes prompt text into a running OpenCode TUI session and streams the assistant final answer back to stdout
|
|
11
|
+
- **`ocmux`** — manages OpenCode server + TUI sessions in tmux (create, switch, kill, list)
|
|
9
12
|
|
|
10
13
|
It is designed for prompt-driven workflows where you want to do things like:
|
|
11
14
|
|
|
@@ -36,8 +39,8 @@ npm link
|
|
|
36
39
|
Notes:
|
|
37
40
|
|
|
38
41
|
- `agentp` connects to the OpenCode event endpoint over HTTP.
|
|
39
|
-
- In practice this means running `opencode
|
|
40
|
-
- `opencode
|
|
42
|
+
- In practice this means running `opencode serve` (or equivalent serve mode) so the port is open.
|
|
43
|
+
- `opencode attach` is optional but useful to monitor the full conversation in another terminal/tmux pane.
|
|
41
44
|
|
|
42
45
|
## Usage
|
|
43
46
|
|
|
@@ -47,7 +50,8 @@ agentp [options] [url]
|
|
|
47
50
|
|
|
48
51
|
Options:
|
|
49
52
|
|
|
50
|
-
- `--qa`: print
|
|
53
|
+
- `--qa`: print the original prompt and answer with labels (useful when used as a filter)
|
|
54
|
+
- `--version`: show version
|
|
51
55
|
- `--help`: show help message
|
|
52
56
|
|
|
53
57
|
Arguments:
|
|
@@ -75,10 +79,10 @@ Send a multi-line prompt from a file:
|
|
|
75
79
|
cat prompt.txt | agentp
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
Use
|
|
82
|
+
Use an explicit port:
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
|
-
cat prompt.txt | agentp
|
|
85
|
+
cat prompt.txt | agentp 4096
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
Capture answer to a file:
|
|
@@ -99,13 +103,53 @@ From Vim/Neovim, send the current visual selection and replace it in place with
|
|
|
99
103
|
:'<,'>!agentp
|
|
100
104
|
```
|
|
101
105
|
|
|
102
|
-
From Vim/Neovim, send the current visual selection and keep
|
|
106
|
+
From Vim/Neovim, send the current visual selection and keep the prompt with the answer:
|
|
103
107
|
|
|
104
108
|
```vim
|
|
105
109
|
:'<,'>!agentp --qa
|
|
106
110
|
```
|
|
107
111
|
|
|
108
|
-
##
|
|
112
|
+
## ocmux
|
|
113
|
+
|
|
114
|
+
Manage OpenCode server + TUI in tmux for project directories.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
ocmux [-l] [<subcommand>] [<directory>]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Without arguments, searches upward from `<directory>` (default: `$PWD`) for `.ocmux.json` and switches to that server's tmux window.
|
|
121
|
+
|
|
122
|
+
Subcommands:
|
|
123
|
+
|
|
124
|
+
- **`new [--git|--GIT] [dir]`** — Create a new server in `dir` (default: `$PWD`). Errors if one already exists there. Warns if a parent directory already has a server.
|
|
125
|
+
- `--git` resolves `dir` to the nearest parent with a `.git` entry (file or dir); errors if none is found.
|
|
126
|
+
- `--GIT` resolves `dir` to the nearest parent with a `.git` directory only; errors if none is found.
|
|
127
|
+
- **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
|
|
128
|
+
- **`list`** — List all running servers with their directories, URLs, and status.
|
|
129
|
+
|
|
130
|
+
Options:
|
|
131
|
+
|
|
132
|
+
- `-l`: long output for default/new commands (append `→ <dir>` after the URL)
|
|
133
|
+
- `--version`: show version
|
|
134
|
+
- `-h`: show help message
|
|
135
|
+
- `--`: treat the next argument as a directory even if it matches a subcommand name
|
|
136
|
+
|
|
137
|
+
Notes:
|
|
138
|
+
|
|
139
|
+
- If `<directory>` is not a valid path, `ocmux` tries to match it against the basenames of existing sessions (exact unique match).
|
|
140
|
+
- When using `--git`/`--GIT`, `ocmux` refuses to create a new server above an existing `.ocmux.json` found while searching for the git root.
|
|
141
|
+
|
|
142
|
+
### Requirements
|
|
143
|
+
|
|
144
|
+
- Node.js 18+
|
|
145
|
+
- [tmux](https://github.com/tmux/tmux) — session multiplexer
|
|
146
|
+
- `opencode` binary in PATH (the OpenCode CLI)
|
|
147
|
+
|
|
148
|
+
### State file
|
|
149
|
+
|
|
150
|
+
`ocmux` stores per-project state in `.ocmux.json` (contains URL, log path, window index). It searches upward from the target directory, similar to git.
|
|
151
|
+
|
|
152
|
+
## How agentp Works
|
|
109
153
|
|
|
110
154
|
1. Clears the current TUI prompt input.
|
|
111
155
|
2. Appends each line from stdin to the TUI prompt.
|
|
@@ -115,7 +159,7 @@ From Vim/Neovim, send the current visual selection and keep QA separators in the
|
|
|
115
159
|
|
|
116
160
|
Operational hint:
|
|
117
161
|
|
|
118
|
-
- You can keep a separate `opencode
|
|
162
|
+
- 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.
|
|
119
163
|
|
|
120
164
|
## License
|
|
121
165
|
|
package/bin/agentp
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const { URL } = require('url');
|
|
5
|
+
const { version } = require('../package.json');
|
|
5
6
|
|
|
6
7
|
function makeRequest(options, data) {
|
|
7
8
|
return new Promise((resolve, reject) => {
|
|
@@ -158,23 +159,30 @@ async function main() {
|
|
|
158
159
|
|
|
159
160
|
const args = process.argv.slice(2);
|
|
160
161
|
for (let i = 0; i < args.length; i++) {
|
|
161
|
-
if (args[i] === '--
|
|
162
|
+
if (args[i] === '--version') {
|
|
163
|
+
console.log(version);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
} else if (args[i] === '--help') {
|
|
162
166
|
console.error('Usage: agentp [options] [url]');
|
|
163
167
|
console.error('');
|
|
164
168
|
console.error('Pipe prompt text to an OpenCode TUI session and stream the assistant response to stdout.');
|
|
165
169
|
console.error('');
|
|
166
170
|
console.error('Options:');
|
|
167
|
-
console.error(' --
|
|
171
|
+
console.error(' --version Show version');
|
|
172
|
+
console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
|
|
168
173
|
console.error(' --help Show this help message');
|
|
169
174
|
console.error('');
|
|
170
175
|
console.error('Arguments:');
|
|
171
176
|
console.error(' url OpenCode TUI server URL or port number (default: 4096)');
|
|
172
177
|
console.error(' Examples: 4096, http://localhost:4096, http://192.168.1.50:4096');
|
|
173
178
|
console.error('');
|
|
179
|
+
console.error('See also: ocmux -- manage OpenCode tmux sessions (ocmux --help)');
|
|
180
|
+
console.error('');
|
|
174
181
|
console.error('Examples:');
|
|
175
182
|
console.error(' printf "Summarize this file" | agentp');
|
|
176
|
-
console.error(' cat prompt.txt | agentp
|
|
177
|
-
console.error('
|
|
183
|
+
console.error(' cat prompt.txt | agentp 4096');
|
|
184
|
+
console.error(' # Vim/Neovim filter, preserving prompt + answer:');
|
|
185
|
+
console.error(" :'<,'>!agentp --qa");
|
|
178
186
|
process.exit(0);
|
|
179
187
|
} else if (args[i] === '--qa') {
|
|
180
188
|
qaMode = true;
|
package/bin/ocmux
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { spawnSync, execSync } = require('child_process');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const { version } = require('../package.json');
|
|
15
|
+
|
|
16
|
+
const SESSION = 'Opencode';
|
|
17
|
+
const SCRIPT = path.basename(process.argv[1] || 'ocmux');
|
|
18
|
+
const HOME = os.homedir();
|
|
19
|
+
const URL_RE = /opencode server listening on (http:\/\/[^\s]*)/;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function tmux(args) {
|
|
26
|
+
const result = spawnSync('tmux', args, { encoding: 'utf8', maxBuffer: 1024 * 1024 });
|
|
27
|
+
if (result.error && result.error.code === 'ENOENT') {
|
|
28
|
+
console.error('Error: tmux is not installed or not in PATH');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sleep(seconds) {
|
|
35
|
+
execSync(`sleep ${seconds}`, { stdio: 'ignore' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hashDir(dir) {
|
|
39
|
+
return crypto.createHash('md5').update(dir).digest('hex').slice(0, 12);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function logfileFor(dir) {
|
|
43
|
+
return `/tmp/opencode-serve-${hashDir(dir)}.log`;
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
+
function findParentStatefile(startDir) {
|
|
59
|
+
let dir = path.resolve(startDir);
|
|
60
|
+
while (true) {
|
|
61
|
+
if (fs.existsSync(path.join(dir, '.ocmux.json'))) {
|
|
62
|
+
return path.join(dir, '.ocmux.json');
|
|
63
|
+
}
|
|
64
|
+
if (dir === HOME || dir === path.dirname(dir)) return null;
|
|
65
|
+
dir = path.dirname(dir);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findParentGitDir(startDir, requireDir) {
|
|
70
|
+
let dir = path.resolve(startDir);
|
|
71
|
+
let stateDir = null;
|
|
72
|
+
while (true) {
|
|
73
|
+
if (!stateDir && fs.existsSync(path.join(dir, '.ocmux.json'))) {
|
|
74
|
+
stateDir = dir;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gitPath = path.join(dir, '.git');
|
|
78
|
+
if (fs.existsSync(gitPath)) {
|
|
79
|
+
if (!requireDir) {
|
|
80
|
+
return { dir, stateDir, gitPath };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
if (fs.statSync(gitPath).isDirectory()) {
|
|
84
|
+
return { dir, stateDir, gitPath };
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore stat errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (dir === HOME || dir === path.dirname(dir)) return { dir: null, stateDir, gitPath: null };
|
|
92
|
+
dir = path.dirname(dir);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveGitDir(startDir, requireDir, flagLabel) {
|
|
97
|
+
const result = findParentGitDir(startDir, requireDir);
|
|
98
|
+
if (!result.dir) {
|
|
99
|
+
const label = requireDir ? '.git directory' : '.git entry';
|
|
100
|
+
console.error(`Error: no ${label} found in ${startDir} or any of its parents.`);
|
|
101
|
+
console.error(`Run '${SCRIPT} new' without ${flagLabel} to create a server in the current directory.`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (result.stateDir && result.stateDir !== result.dir) {
|
|
106
|
+
console.error(`Error: found an existing ocmux server in ${result.stateDir}.`);
|
|
107
|
+
console.error(`Refusing to create a new server at ${result.dir} because it is above that session.`);
|
|
108
|
+
console.error(`Move outside ${result.stateDir} or pass an explicit directory.`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result.dir;
|
|
113
|
+
}
|
|
114
|
+
|
|
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
|
+
function findWindowsByBasename(name) {
|
|
162
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_name}']);
|
|
163
|
+
if (r.status !== 0) return [];
|
|
164
|
+
return r.stdout.trim().split('\n').filter(Boolean).filter(w => path.basename(w) === name);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveDir(arg) {
|
|
168
|
+
const resolved = path.resolve(arg);
|
|
169
|
+
try {
|
|
170
|
+
if (fs.statSync(resolved).isDirectory()) return resolved;
|
|
171
|
+
} catch {
|
|
172
|
+
// not a valid path or inaccessible
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!arg.includes('/')) {
|
|
176
|
+
const matches = findWindowsByBasename(arg);
|
|
177
|
+
if (matches.length === 1) return matches[0];
|
|
178
|
+
if (matches.length > 1) {
|
|
179
|
+
console.error(`Error: more than one match for '${arg}':`);
|
|
180
|
+
for (const m of matches) console.error(` ${m}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.error(`Error: invalid directory '${arg}'`);
|
|
186
|
+
console.error(`Run '${SCRIPT} -h' for help.`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ensureTuiZoomed(windowIndex) {
|
|
191
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}|#{window_zoomed_flag}']);
|
|
192
|
+
if (r.status !== 0) return;
|
|
193
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
194
|
+
const [wi, zoomed] = line.split('|');
|
|
195
|
+
if (parseInt(wi, 10) === windowIndex && zoomed === '1') return;
|
|
196
|
+
}
|
|
197
|
+
const paneId = tuiPaneId(windowIndex);
|
|
198
|
+
if (paneId) tmux(['resize-pane', '-Z', '-t', paneId]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pinWindowName(windowIndex) {
|
|
202
|
+
tmux(['set-window-option', '-t', `${SESSION}:${windowIndex}`, 'automatic-rename', 'off']);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function ensureSession() {
|
|
206
|
+
const r = tmux(['has-session', '-t', SESSION]);
|
|
207
|
+
if (r.status !== 0) {
|
|
208
|
+
const rc = tmux(['new-session', '-d', '-s', SESSION]);
|
|
209
|
+
if (rc.status !== 0) {
|
|
210
|
+
console.error('Error: failed to create tmux session');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function outputUrl(url, dir, longMode) {
|
|
217
|
+
if (longMode) {
|
|
218
|
+
console.log(`${url} → ${dir}`);
|
|
219
|
+
} else {
|
|
220
|
+
console.log(url);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function printHelp() {
|
|
225
|
+
console.error(`Usage: ${SCRIPT} [-l] [<subcommand>] [<directory>]`);
|
|
226
|
+
console.error('');
|
|
227
|
+
console.error('Manage opencode server + TUI in tmux for project directories.');
|
|
228
|
+
console.error('');
|
|
229
|
+
console.error('Without arguments, searches upward from <directory> (default: $PWD)');
|
|
230
|
+
console.error("for .ocmux.json and switches to that server's tmux window.");
|
|
231
|
+
console.error('If <directory> is not a valid path, ocmux will match it against the');
|
|
232
|
+
console.error("basenames of existing sessions' directories. If exactly one matches,");
|
|
233
|
+
console.error('that session is used.');
|
|
234
|
+
console.error('');
|
|
235
|
+
console.error('Subcommands:');
|
|
236
|
+
console.error(' new Create a new server in <directory> (default: $PWD).');
|
|
237
|
+
console.error(' Errors if one already exists there.');
|
|
238
|
+
console.error(' Warns if a parent directory already has a server.');
|
|
239
|
+
console.error(' --git Use the nearest parent with a .git entry (file or dir).');
|
|
240
|
+
console.error(' --GIT Use the nearest parent with a .git directory only.');
|
|
241
|
+
console.error(' Refuses to create above an existing .ocmux.json found while searching.');
|
|
242
|
+
console.error(' kill Kill the server found upward from <directory>.');
|
|
243
|
+
console.error(' Removes its tmux window and state file.');
|
|
244
|
+
console.error(' list List all running servers with their directories and URLs.');
|
|
245
|
+
console.error('');
|
|
246
|
+
console.error(' -- Treat the next argument as a directory even if it matches');
|
|
247
|
+
console.error(' a subcommand name (e.g. "ocmux -- kill").');
|
|
248
|
+
console.error('');
|
|
249
|
+
console.error('Options:');
|
|
250
|
+
console.error(' -l Long output: append " → <dir>" after the URL (new/default).');
|
|
251
|
+
console.error(' --version Show version');
|
|
252
|
+
console.error(' -h Show this help');
|
|
253
|
+
console.error('');
|
|
254
|
+
console.error('See also: agentp -- pipe prompts into OpenCode from stdin');
|
|
255
|
+
process.exit(0);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Subcommand: default — switch to an existing server
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function cmdSwitch(statefile, longMode) {
|
|
263
|
+
const serverDir = path.dirname(statefile);
|
|
264
|
+
const state = readState(statefile);
|
|
265
|
+
|
|
266
|
+
if (!state || !state.url || state.window_index == null) {
|
|
267
|
+
console.error(`Error: invalid state file ${statefile}`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let { url, window_index: winIdx } = state;
|
|
272
|
+
|
|
273
|
+
const actualName = windowNameByIndex(winIdx);
|
|
274
|
+
if (actualName !== serverDir) {
|
|
275
|
+
const found = windowByDir(serverDir);
|
|
276
|
+
if (found == null) {
|
|
277
|
+
console.error(`Error: tmux window for ${serverDir} not found`);
|
|
278
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
winIdx = found;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
pinWindowName(winIdx);
|
|
285
|
+
|
|
286
|
+
const tuiPane = tuiPaneId(winIdx);
|
|
287
|
+
if (!tuiPane) {
|
|
288
|
+
console.error('TUI pane is dead, restarting...');
|
|
289
|
+
const count = paneCount(winIdx);
|
|
290
|
+
if (count >= 2) {
|
|
291
|
+
const panesR = tmux(['list-panes', '-t', `${SESSION}:${winIdx}`, '-F', '#{pane_id}']);
|
|
292
|
+
const panes = panesR.stdout.trim().split('\n').filter(Boolean);
|
|
293
|
+
const lastPane = panes[panes.length - 1];
|
|
294
|
+
const respawnR = tmux(['respawn-pane', '-k', '-t', lastPane, `opencode attach --continue '${url}'`]);
|
|
295
|
+
if (respawnR.status !== 0) {
|
|
296
|
+
tmux(['send-keys', '-t', lastPane, `opencode attach --continue '${url}'`, 'Enter']);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
tmux(['split-window', '-v', '-t', `${SESSION}:${winIdx}`, '-c', serverDir,
|
|
300
|
+
`opencode attach --continue '${url}'`]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const activeWin = activeWindowIndex();
|
|
305
|
+
if (activeWin !== winIdx) {
|
|
306
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
ensureTuiZoomed(winIdx);
|
|
310
|
+
outputUrl(url, serverDir, longMode);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Subcommand: list
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function cmdList() {
|
|
318
|
+
const sessionR = tmux(['has-session', '-t', SESSION]);
|
|
319
|
+
if (sessionR.status !== 0) {
|
|
320
|
+
console.log('No opencode servers running.');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const listR = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
325
|
+
if (listR.status !== 0 || !listR.stdout.trim()) {
|
|
326
|
+
console.log('No opencode servers running.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const rows = [];
|
|
331
|
+
for (const line of listR.stdout.trim().split('\n').filter(Boolean)) {
|
|
332
|
+
const [idx, dir] = line.split('\t');
|
|
333
|
+
const sf = path.join(dir, '.ocmux.json');
|
|
334
|
+
if (!fs.existsSync(sf)) continue;
|
|
335
|
+
const state = readState(sf);
|
|
336
|
+
if (!state || !state.url) continue;
|
|
337
|
+
const tui = tuiPaneId(parseInt(idx, 10));
|
|
338
|
+
rows.push({ url: state.url, dir, status: tui ? 'alive' : 'dead' });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (rows.length === 0) {
|
|
342
|
+
console.log('No opencode servers running.');
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const urlW = Math.max(...rows.map(r => r.url.length), 3);
|
|
347
|
+
const dirW = Math.max(...rows.map(r => r.dir.length), 9);
|
|
348
|
+
const stsW = Math.max(...rows.map(r => r.status.length), 6);
|
|
349
|
+
|
|
350
|
+
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
|
351
|
+
|
|
352
|
+
console.log(`${pad('URL', urlW)} ${pad('Directory', dirW)} ${pad('Status', stsW)}`);
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
console.log(`${pad(row.url, urlW)} ${pad(row.dir, dirW)} ${pad(row.status, stsW)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Subcommand: new
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
function cmdNew(targetDir, longMode) {
|
|
363
|
+
const sf = path.join(targetDir, '.ocmux.json');
|
|
364
|
+
if (fs.existsSync(sf)) {
|
|
365
|
+
console.error(`Error: a server already exists in ${targetDir}.`);
|
|
366
|
+
console.error("Run 'ocmux' without arguments to switch to it, or 'ocmux kill' to stop it first.");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const parent = findParentStatefile(path.dirname(targetDir));
|
|
371
|
+
if (parent) {
|
|
372
|
+
const parentDir = path.dirname(parent);
|
|
373
|
+
console.error(`Warning: there is already an opencode server in ${parentDir}.`);
|
|
374
|
+
console.error("Running 'ocmux' without arguments would switch to that server.");
|
|
375
|
+
|
|
376
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
377
|
+
rl.question('Are you sure you want to create a new server here? [y/N] ', (answer) => {
|
|
378
|
+
rl.close();
|
|
379
|
+
if (!/^[yY]([eE][sS])?$/.test(answer.trim())) {
|
|
380
|
+
console.error('Cancelled.');
|
|
381
|
+
process.exit(0);
|
|
382
|
+
}
|
|
383
|
+
doNew(targetDir, longMode);
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
doNew(targetDir, longMode);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function doNew(targetDir, longMode) {
|
|
391
|
+
ensureSession();
|
|
392
|
+
const lf = logfileFor(targetDir);
|
|
393
|
+
|
|
394
|
+
console.error(`Starting opencode server for ${targetDir}...`);
|
|
395
|
+
|
|
396
|
+
const rc = tmux(['new-window', '-d', '-t', SESSION, '-n', targetDir, '-c', targetDir,
|
|
397
|
+
`opencode serve --port 0 2>&1 | tee '${lf}'`]);
|
|
398
|
+
if (rc.status !== 0) {
|
|
399
|
+
console.error(`Error: failed to create tmux window: ${rc.stderr}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Wait up to 10s for the URL in the logfile
|
|
404
|
+
let url = null;
|
|
405
|
+
for (let i = 0; i < 50; i++) {
|
|
406
|
+
sleep(0.2);
|
|
407
|
+
const log = (() => { try { return fs.readFileSync(lf, 'utf8'); } catch { return ''; } })();
|
|
408
|
+
const m = log.match(URL_RE);
|
|
409
|
+
if (m) { url = m[1]; break; }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!url) {
|
|
413
|
+
console.error('Error: opencode server did not start within 10 seconds');
|
|
414
|
+
const wi = windowByDir(targetDir);
|
|
415
|
+
if (wi != null) tmux(['kill-window', '-t', `${SESSION}:${wi}`]);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const winIdx = windowByDir(targetDir);
|
|
420
|
+
if (winIdx == null) {
|
|
421
|
+
console.error('Error: window was not created properly');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pinWindowName(winIdx);
|
|
426
|
+
|
|
427
|
+
const state = { url, logfile: lf, window_index: winIdx };
|
|
428
|
+
fs.writeFileSync(path.join(targetDir, '.ocmux.json'), JSON.stringify(state) + '\n');
|
|
429
|
+
|
|
430
|
+
tmux(['split-window', '-v', '-t', `${SESSION}:${winIdx}`, '-c', targetDir,
|
|
431
|
+
`opencode attach --continue '${url}'`]);
|
|
432
|
+
sleep(0.5);
|
|
433
|
+
ensureTuiZoomed(winIdx);
|
|
434
|
+
|
|
435
|
+
const activeWin = activeWindowIndex();
|
|
436
|
+
if (activeWin !== winIdx) {
|
|
437
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
outputUrl(url, targetDir, longMode);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// Subcommand: kill
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
function cmdKill(statefile) {
|
|
448
|
+
const serverDir = path.dirname(statefile);
|
|
449
|
+
const state = readState(statefile);
|
|
450
|
+
|
|
451
|
+
if (!state || !state.url || state.window_index == null) {
|
|
452
|
+
console.error(`Error: invalid state file ${statefile}`);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let { window_index: winIdx } = state;
|
|
457
|
+
|
|
458
|
+
console.error(`Stopping server and removing window for ${serverDir}...`);
|
|
459
|
+
|
|
460
|
+
const sessionR = tmux(['has-session', '-t', SESSION]);
|
|
461
|
+
if (sessionR.status === 0) {
|
|
462
|
+
const actualName = windowNameByIndex(winIdx);
|
|
463
|
+
if (actualName !== serverDir) {
|
|
464
|
+
const found = windowByDir(serverDir);
|
|
465
|
+
if (found != null) winIdx = found;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (winIdx != null) {
|
|
469
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
470
|
+
tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`, 'C-c']);
|
|
471
|
+
sleep(0.3);
|
|
472
|
+
tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
|
|
473
|
+
console.error('Window removed.');
|
|
474
|
+
} else {
|
|
475
|
+
console.error(`Tmux window not found for ${serverDir} (cleaning up state file).`);
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
console.error("No tmux session '" + SESSION + "' found (cleaning up state file).");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fs.unlinkSync(statefile);
|
|
482
|
+
console.error('Done.');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Argument parsing
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
function main() {
|
|
490
|
+
let longMode = false;
|
|
491
|
+
let gitMode = null;
|
|
492
|
+
const positional = [];
|
|
493
|
+
let dirIsForced = false;
|
|
494
|
+
|
|
495
|
+
for (const arg of process.argv.slice(2)) {
|
|
496
|
+
if (arg === '-l') {
|
|
497
|
+
longMode = true;
|
|
498
|
+
} else if (arg === '--git') {
|
|
499
|
+
if (gitMode && gitMode !== 'nearest') {
|
|
500
|
+
console.error("Error: cannot combine '--git' with '--GIT'.");
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
gitMode = 'nearest';
|
|
504
|
+
} else if (arg === '--GIT') {
|
|
505
|
+
if (gitMode && gitMode !== 'root') {
|
|
506
|
+
console.error("Error: cannot combine '--git' with '--GIT'.");
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
gitMode = 'root';
|
|
510
|
+
} else if (arg === '--version') {
|
|
511
|
+
console.log(version);
|
|
512
|
+
process.exit(0);
|
|
513
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
514
|
+
printHelp();
|
|
515
|
+
} else if (arg === '--') {
|
|
516
|
+
dirIsForced = true;
|
|
517
|
+
} else if (arg.startsWith('-')) {
|
|
518
|
+
if (arg === '--new' || arg === '--kill') {
|
|
519
|
+
console.error(`Error: '${arg}' is no longer a flag. Use 'ocmux ${arg.slice(2)}' instead.`);
|
|
520
|
+
} else {
|
|
521
|
+
console.error(`Error: unknown option '${arg}'`);
|
|
522
|
+
console.error(`Run '${SCRIPT} -h' for help.`);
|
|
523
|
+
}
|
|
524
|
+
process.exit(1);
|
|
525
|
+
} else {
|
|
526
|
+
positional.push(arg);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let subcommand = null;
|
|
531
|
+
let targetDir = null;
|
|
532
|
+
|
|
533
|
+
for (const arg of positional) {
|
|
534
|
+
if (dirIsForced) {
|
|
535
|
+
dirIsForced = false;
|
|
536
|
+
if (targetDir) {
|
|
537
|
+
console.error(`Error: unexpected argument '${arg}'`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
targetDir = resolveDir(arg);
|
|
541
|
+
} else if (arg === 'new' || arg === 'kill' || arg === 'list') {
|
|
542
|
+
if (subcommand) {
|
|
543
|
+
console.error(`Error: multiple subcommands specified: '${subcommand}' and '${arg}'`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
subcommand = arg;
|
|
547
|
+
} else {
|
|
548
|
+
if (targetDir) {
|
|
549
|
+
console.error(`Error: unexpected argument '${arg}'`);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
targetDir = resolveDir(arg);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!targetDir) targetDir = process.cwd();
|
|
557
|
+
|
|
558
|
+
// Dispatch
|
|
559
|
+
if (subcommand === 'list') {
|
|
560
|
+
if (gitMode) {
|
|
561
|
+
console.error("Error: '--git' and '--GIT' are only valid with 'new'.");
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
if (targetDir !== process.cwd()) {
|
|
565
|
+
console.error("Error: 'list' does not accept a directory path.");
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
cmdList();
|
|
569
|
+
} else if (subcommand === 'kill') {
|
|
570
|
+
if (gitMode) {
|
|
571
|
+
console.error("Error: '--git' and '--GIT' are only valid with 'new'.");
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
const sf = findParentStatefile(targetDir);
|
|
575
|
+
if (!sf) {
|
|
576
|
+
console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
|
|
577
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
cmdKill(sf);
|
|
581
|
+
} else if (subcommand === 'new') {
|
|
582
|
+
if (gitMode) {
|
|
583
|
+
const flagLabel = gitMode === 'root' ? '--GIT' : '--git';
|
|
584
|
+
targetDir = resolveGitDir(targetDir, gitMode === 'root', flagLabel);
|
|
585
|
+
}
|
|
586
|
+
cmdNew(targetDir, longMode);
|
|
587
|
+
} else {
|
|
588
|
+
if (gitMode) {
|
|
589
|
+
console.error("Error: '--git' and '--GIT' are only valid with 'new'.");
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
// Default mode
|
|
593
|
+
const sf = findParentStatefile(targetDir);
|
|
594
|
+
if (!sf) {
|
|
595
|
+
console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
|
|
596
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
ensureSession();
|
|
600
|
+
cmdSwitch(sf, longMode);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Pipe prompts into a running OpenCode TUI session and stream the final answer",
|
|
5
5
|
"bin": {
|
|
6
|
-
"agentp": "bin/agentp"
|
|
6
|
+
"agentp": "bin/agentp",
|
|
7
|
+
"ocmux": "bin/ocmux"
|
|
7
8
|
},
|
|
8
9
|
"keywords": [
|
|
9
10
|
"ai",
|
package/.ocmux.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"url":"http://127.0.0.1:4096","logfile":"/tmp/opencode-serve-6cdb98e4626e.log","window_index":1}
|