agentp 0.1.1 → 0.2.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 +48 -0
- package/README.md +31 -2
- package/bin/agentp +8 -1
- package/bin/ocmux +526 -0
- package/package.json +3 -2
package/AGENTS.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# agentp — agent notes
|
|
2
|
+
|
|
3
|
+
## Two binaries, both registered
|
|
4
|
+
|
|
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
|
|
7
|
+
|
|
8
|
+
## Package facts
|
|
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.
|
|
15
|
+
|
|
16
|
+
## `.ocmux.json`
|
|
17
|
+
|
|
18
|
+
Runtime state file created by `bin/ocmux` (contains URL, log path, and window index). **Not in `.gitignore`** — avoid committing it.
|
|
19
|
+
|
|
20
|
+
## OpenCode HTTP protocol (agentp)
|
|
21
|
+
|
|
22
|
+
These endpoints are consumed but not documented elsewhere in the repo:
|
|
23
|
+
|
|
24
|
+
- `POST /tui/clear-prompt`
|
|
25
|
+
- `POST /tui/append-prompt`
|
|
26
|
+
- `POST /tui/submit-prompt`
|
|
27
|
+
- `GET /event` (SSE stream)
|
|
28
|
+
|
|
29
|
+
## OpenCode tmux session model (ocmux)
|
|
30
|
+
|
|
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
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# agentp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides two CLI tools:
|
|
4
|
+
|
|
5
|
+
- **`agentp`** — pipes prompt text into a running OpenCode TUI session and streams the assistant final answer back to stdout
|
|
6
|
+
- **`ocmux`** — manages OpenCode server + TUI sessions in tmux (create, switch, kill, list)
|
|
4
7
|
|
|
5
8
|
It is designed for prompt-driven workflows where you want to do things like:
|
|
6
9
|
|
|
@@ -94,7 +97,33 @@ From Vim/Neovim, send the current visual selection and keep QA separators in the
|
|
|
94
97
|
:'<,'>!agentp --qa
|
|
95
98
|
```
|
|
96
99
|
|
|
97
|
-
##
|
|
100
|
+
## ocmux
|
|
101
|
+
|
|
102
|
+
Manage OpenCode server + TUI in tmux for project directories.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
ocmux [-l] [<subcommand>] [<directory>]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Without arguments, searches upward from `<directory>` (default: `$PWD`) for `.ocmux.json` and switches to that server's tmux window.
|
|
109
|
+
|
|
110
|
+
Subcommands:
|
|
111
|
+
|
|
112
|
+
- **`new [dir]`** — Create a new server in `dir` (default: `$PWD`). Errors if one already exists there. Warns if a parent directory already has a server.
|
|
113
|
+
- **`kill [dir]`** — Kill the server found upward from `dir`. Removes its tmux window and state file.
|
|
114
|
+
- **`list [-l]`** — List all running servers with their directories, URLs, and status.
|
|
115
|
+
|
|
116
|
+
### Requirements
|
|
117
|
+
|
|
118
|
+
- Node.js 18+
|
|
119
|
+
- [tmux](https://github.com/tmux/tmux) — session multiplexer
|
|
120
|
+
- `opencode` binary in PATH (the OpenCode CLI)
|
|
121
|
+
|
|
122
|
+
### State file
|
|
123
|
+
|
|
124
|
+
`ocmux` stores per-project state in `.ocmux.json` (contains URL, log path, window index). It searches upward from the target directory, similar to git.
|
|
125
|
+
|
|
126
|
+
## How agentp Works
|
|
98
127
|
|
|
99
128
|
1. Clears the current TUI prompt input.
|
|
100
129
|
2. Appends each line from stdin to the TUI prompt.
|
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) => {
|
|
@@ -159,19 +160,25 @@ async function main() {
|
|
|
159
160
|
|
|
160
161
|
const args = process.argv.slice(2);
|
|
161
162
|
for (let i = 0; i < args.length; i++) {
|
|
162
|
-
if (args[i] === '--
|
|
163
|
+
if (args[i] === '--version') {
|
|
164
|
+
console.log(version);
|
|
165
|
+
process.exit(0);
|
|
166
|
+
} else if (args[i] === '--help') {
|
|
163
167
|
console.error('Usage: agentp [options] [port]');
|
|
164
168
|
console.error('');
|
|
165
169
|
console.error('Pipe prompt text to an OpenCode TUI session running on localhost.');
|
|
166
170
|
console.error('The assistant response is streamed to stdout.');
|
|
167
171
|
console.error('');
|
|
168
172
|
console.error('Options:');
|
|
173
|
+
console.error(' --version Show version');
|
|
169
174
|
console.error(' --qa Print question/answer separators before streaming output');
|
|
170
175
|
console.error(' --help Show this help message');
|
|
171
176
|
console.error('');
|
|
172
177
|
console.error('Arguments:');
|
|
173
178
|
console.error(' port OpenCode TUI port (default: 4096)');
|
|
174
179
|
console.error('');
|
|
180
|
+
console.error('See also: ocmux -- manage OpenCode tmux sessions (ocmux --help)');
|
|
181
|
+
console.error('');
|
|
175
182
|
console.error('Examples:');
|
|
176
183
|
console.error(' printf "Summarize this file" | agentp');
|
|
177
184
|
console.error(' cat prompt.txt | agentp --qa 4096');
|
package/bin/ocmux
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
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 windowByDir(dir) {
|
|
70
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
71
|
+
if (r.status !== 0) return null;
|
|
72
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
73
|
+
const [idx, name] = line.split('\t');
|
|
74
|
+
if (name === dir) return parseInt(idx, 10);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function windowNameByIndex(idx) {
|
|
80
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
81
|
+
if (r.status !== 0) return null;
|
|
82
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
83
|
+
const [wi, name] = line.split('\t');
|
|
84
|
+
if (parseInt(wi, 10) === idx) return name;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function activeWindowIndex() {
|
|
90
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index} #{window_active}']);
|
|
91
|
+
if (r.status !== 0) return null;
|
|
92
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
93
|
+
const [idx, active] = line.split(' ');
|
|
94
|
+
if (active === '1') return parseInt(idx, 10);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tuiPaneId(windowIndex) {
|
|
100
|
+
const r = tmux(['list-panes', '-t', `${SESSION}:${windowIndex}`, '-F', '#{pane_id}\t#{pane_current_command}']);
|
|
101
|
+
if (r.status !== 0) return null;
|
|
102
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
103
|
+
const [paneId, cmd] = line.split('\t');
|
|
104
|
+
if (cmd === 'node') return paneId;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function paneCount(windowIndex) {
|
|
110
|
+
const r = tmux(['list-panes', '-t', `${SESSION}:${windowIndex}`, '-F', '#{pane_id}']);
|
|
111
|
+
if (r.status !== 0) return 0;
|
|
112
|
+
return r.stdout.trim().split('\n').filter(Boolean).length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findWindowsByBasename(name) {
|
|
116
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_name}']);
|
|
117
|
+
if (r.status !== 0) return [];
|
|
118
|
+
return r.stdout.trim().split('\n').filter(Boolean).filter(w => path.basename(w) === name);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveDir(arg) {
|
|
122
|
+
const resolved = path.resolve(arg);
|
|
123
|
+
try {
|
|
124
|
+
if (fs.statSync(resolved).isDirectory()) return resolved;
|
|
125
|
+
} catch {
|
|
126
|
+
// not a valid path or inaccessible
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!arg.includes('/')) {
|
|
130
|
+
const matches = findWindowsByBasename(arg);
|
|
131
|
+
if (matches.length === 1) return matches[0];
|
|
132
|
+
if (matches.length > 1) {
|
|
133
|
+
console.error(`Error: more than one match for '${arg}':`);
|
|
134
|
+
for (const m of matches) console.error(` ${m}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.error(`Error: invalid directory '${arg}'`);
|
|
140
|
+
console.error(`Run '${SCRIPT} -h' for help.`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ensureTuiZoomed(windowIndex) {
|
|
145
|
+
const r = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}|#{window_zoomed_flag}']);
|
|
146
|
+
if (r.status !== 0) return;
|
|
147
|
+
for (const line of r.stdout.trim().split('\n').filter(Boolean)) {
|
|
148
|
+
const [wi, zoomed] = line.split('|');
|
|
149
|
+
if (parseInt(wi, 10) === windowIndex && zoomed === '1') return;
|
|
150
|
+
}
|
|
151
|
+
const paneId = tuiPaneId(windowIndex);
|
|
152
|
+
if (paneId) tmux(['resize-pane', '-Z', '-t', paneId]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pinWindowName(windowIndex) {
|
|
156
|
+
tmux(['set-window-option', '-t', `${SESSION}:${windowIndex}`, 'automatic-rename', 'off']);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureSession() {
|
|
160
|
+
const r = tmux(['has-session', '-t', SESSION]);
|
|
161
|
+
if (r.status !== 0) {
|
|
162
|
+
const rc = tmux(['new-session', '-d', '-s', SESSION]);
|
|
163
|
+
if (rc.status !== 0) {
|
|
164
|
+
console.error('Error: failed to create tmux session');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function outputUrl(url, dir, longMode) {
|
|
171
|
+
if (longMode) {
|
|
172
|
+
console.log(`${url} → ${dir}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(url);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printHelp() {
|
|
179
|
+
console.error(`Usage: ${SCRIPT} [-l] [<subcommand>] [<directory>]`);
|
|
180
|
+
console.error('');
|
|
181
|
+
console.error('Manage opencode server + TUI in tmux for project directories.');
|
|
182
|
+
console.error('');
|
|
183
|
+
console.error('Without arguments, searches upward from <directory> (default: $PWD)');
|
|
184
|
+
console.error("for .ocmux.json and switches to that server's tmux window.");
|
|
185
|
+
console.error('If <directory> is not a valid path, ocmux will match it against the');
|
|
186
|
+
console.error("basenames of existing sessions' directories. If exactly one matches,");
|
|
187
|
+
console.error('that session is used.');
|
|
188
|
+
console.error('');
|
|
189
|
+
console.error('Subcommands:');
|
|
190
|
+
console.error(' new Create a new server in <directory> (default: $PWD).');
|
|
191
|
+
console.error(' Errors if one already exists there.');
|
|
192
|
+
console.error(' Warns if a parent directory already has a server.');
|
|
193
|
+
console.error(' kill Kill the server found upward from <directory>.');
|
|
194
|
+
console.error(' Removes its tmux window and state file.');
|
|
195
|
+
console.error(' list List all running servers with their directories and URLs.');
|
|
196
|
+
console.error('');
|
|
197
|
+
console.error(' -- Treat the next argument as a directory even if it matches');
|
|
198
|
+
console.error(' a subcommand name (e.g. "ocmux -- kill").');
|
|
199
|
+
console.error('');
|
|
200
|
+
console.error('Options:');
|
|
201
|
+
console.error(' -l Long output: append " → <dir>" after the URL.');
|
|
202
|
+
console.error(' --version Show version');
|
|
203
|
+
console.error(' -h Show this help');
|
|
204
|
+
console.error('');
|
|
205
|
+
console.error('See also: agentp -- pipe prompts into OpenCode from stdin');
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Subcommand: default — switch to an existing server
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function cmdSwitch(statefile, longMode) {
|
|
214
|
+
const serverDir = path.dirname(statefile);
|
|
215
|
+
const state = readState(statefile);
|
|
216
|
+
|
|
217
|
+
if (!state || !state.url || state.window_index == null) {
|
|
218
|
+
console.error(`Error: invalid state file ${statefile}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let { url, window_index: winIdx } = state;
|
|
223
|
+
|
|
224
|
+
const actualName = windowNameByIndex(winIdx);
|
|
225
|
+
if (actualName !== serverDir) {
|
|
226
|
+
const found = windowByDir(serverDir);
|
|
227
|
+
if (found == null) {
|
|
228
|
+
console.error(`Error: tmux window for ${serverDir} not found`);
|
|
229
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
winIdx = found;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
pinWindowName(winIdx);
|
|
236
|
+
|
|
237
|
+
const tuiPane = tuiPaneId(winIdx);
|
|
238
|
+
if (!tuiPane) {
|
|
239
|
+
console.error('TUI pane is dead, restarting...');
|
|
240
|
+
const count = paneCount(winIdx);
|
|
241
|
+
if (count >= 2) {
|
|
242
|
+
const panesR = tmux(['list-panes', '-t', `${SESSION}:${winIdx}`, '-F', '#{pane_id}']);
|
|
243
|
+
const panes = panesR.stdout.trim().split('\n').filter(Boolean);
|
|
244
|
+
const lastPane = panes[panes.length - 1];
|
|
245
|
+
const respawnR = tmux(['respawn-pane', '-k', '-t', lastPane, `opencode attach --continue '${url}'`]);
|
|
246
|
+
if (respawnR.status !== 0) {
|
|
247
|
+
tmux(['send-keys', '-t', lastPane, `opencode attach --continue '${url}'`, 'Enter']);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
tmux(['split-window', '-v', '-t', `${SESSION}:${winIdx}`, '-c', serverDir,
|
|
251
|
+
`opencode attach --continue '${url}'`]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const activeWin = activeWindowIndex();
|
|
256
|
+
if (activeWin !== winIdx) {
|
|
257
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
ensureTuiZoomed(winIdx);
|
|
261
|
+
outputUrl(url, serverDir, longMode);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Subcommand: list
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
function cmdList() {
|
|
269
|
+
const sessionR = tmux(['has-session', '-t', SESSION]);
|
|
270
|
+
if (sessionR.status !== 0) {
|
|
271
|
+
console.log('No opencode servers running.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const listR = tmux(['list-windows', '-t', SESSION, '-F', '#{window_index}\t#{window_name}']);
|
|
276
|
+
if (listR.status !== 0 || !listR.stdout.trim()) {
|
|
277
|
+
console.log('No opencode servers running.');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const rows = [];
|
|
282
|
+
for (const line of listR.stdout.trim().split('\n').filter(Boolean)) {
|
|
283
|
+
const [idx, dir] = line.split('\t');
|
|
284
|
+
const sf = path.join(dir, '.ocmux.json');
|
|
285
|
+
if (!fs.existsSync(sf)) continue;
|
|
286
|
+
const state = readState(sf);
|
|
287
|
+
if (!state || !state.url) continue;
|
|
288
|
+
const tui = tuiPaneId(parseInt(idx, 10));
|
|
289
|
+
rows.push({ url: state.url, dir, status: tui ? 'alive' : 'dead' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (rows.length === 0) {
|
|
293
|
+
console.log('No opencode servers running.');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const urlW = Math.max(...rows.map(r => r.url.length), 3);
|
|
298
|
+
const dirW = Math.max(...rows.map(r => r.dir.length), 9);
|
|
299
|
+
const stsW = Math.max(...rows.map(r => r.status.length), 6);
|
|
300
|
+
|
|
301
|
+
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
|
302
|
+
|
|
303
|
+
console.log(`${pad('URL', urlW)} ${pad('Directory', dirW)} ${pad('Status', stsW)}`);
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
console.log(`${pad(row.url, urlW)} ${pad(row.dir, dirW)} ${pad(row.status, stsW)}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Subcommand: new
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
function cmdNew(targetDir, longMode) {
|
|
314
|
+
const sf = path.join(targetDir, '.ocmux.json');
|
|
315
|
+
if (fs.existsSync(sf)) {
|
|
316
|
+
console.error(`Error: a server already exists in ${targetDir}.`);
|
|
317
|
+
console.error("Run 'ocmux' without arguments to switch to it, or 'ocmux kill' to stop it first.");
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const parent = findParentStatefile(path.dirname(targetDir));
|
|
322
|
+
if (parent) {
|
|
323
|
+
const parentDir = path.dirname(parent);
|
|
324
|
+
console.error(`Warning: there is already an opencode server in ${parentDir}.`);
|
|
325
|
+
console.error("Running 'ocmux' without arguments would switch to that server.");
|
|
326
|
+
|
|
327
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
328
|
+
rl.question('Are you sure you want to create a new server here? [y/N] ', (answer) => {
|
|
329
|
+
rl.close();
|
|
330
|
+
if (!/^[yY]([eE][sS])?$/.test(answer.trim())) {
|
|
331
|
+
console.error('Cancelled.');
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
doNew(targetDir, longMode);
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
doNew(targetDir, longMode);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function doNew(targetDir, longMode) {
|
|
342
|
+
ensureSession();
|
|
343
|
+
const lf = logfileFor(targetDir);
|
|
344
|
+
|
|
345
|
+
console.error(`Starting opencode server for ${targetDir}...`);
|
|
346
|
+
|
|
347
|
+
const rc = tmux(['new-window', '-d', '-t', SESSION, '-n', targetDir, '-c', targetDir,
|
|
348
|
+
`opencode serve --port 0 2>&1 | tee '${lf}'`]);
|
|
349
|
+
if (rc.status !== 0) {
|
|
350
|
+
console.error(`Error: failed to create tmux window: ${rc.stderr}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Wait up to 10s for the URL in the logfile
|
|
355
|
+
let url = null;
|
|
356
|
+
for (let i = 0; i < 50; i++) {
|
|
357
|
+
sleep(0.2);
|
|
358
|
+
const log = (() => { try { return fs.readFileSync(lf, 'utf8'); } catch { return ''; } })();
|
|
359
|
+
const m = log.match(URL_RE);
|
|
360
|
+
if (m) { url = m[1]; break; }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!url) {
|
|
364
|
+
console.error('Error: opencode server did not start within 10 seconds');
|
|
365
|
+
const wi = windowByDir(targetDir);
|
|
366
|
+
if (wi != null) tmux(['kill-window', '-t', `${SESSION}:${wi}`]);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const winIdx = windowByDir(targetDir);
|
|
371
|
+
if (winIdx == null) {
|
|
372
|
+
console.error('Error: window was not created properly');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
pinWindowName(winIdx);
|
|
377
|
+
|
|
378
|
+
const state = { url, logfile: lf, window_index: winIdx };
|
|
379
|
+
fs.writeFileSync(path.join(targetDir, '.ocmux.json'), JSON.stringify(state) + '\n');
|
|
380
|
+
|
|
381
|
+
tmux(['split-window', '-v', '-t', `${SESSION}:${winIdx}`, '-c', targetDir,
|
|
382
|
+
`opencode attach --continue '${url}'`]);
|
|
383
|
+
sleep(0.5);
|
|
384
|
+
ensureTuiZoomed(winIdx);
|
|
385
|
+
|
|
386
|
+
const activeWin = activeWindowIndex();
|
|
387
|
+
if (activeWin !== winIdx) {
|
|
388
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
outputUrl(url, targetDir, longMode);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Subcommand: kill
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
function cmdKill(statefile) {
|
|
399
|
+
const serverDir = path.dirname(statefile);
|
|
400
|
+
const state = readState(statefile);
|
|
401
|
+
|
|
402
|
+
if (!state || !state.url || state.window_index == null) {
|
|
403
|
+
console.error(`Error: invalid state file ${statefile}`);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let { window_index: winIdx } = state;
|
|
408
|
+
|
|
409
|
+
console.error(`Stopping server and removing window for ${serverDir}...`);
|
|
410
|
+
|
|
411
|
+
const sessionR = tmux(['has-session', '-t', SESSION]);
|
|
412
|
+
if (sessionR.status === 0) {
|
|
413
|
+
const actualName = windowNameByIndex(winIdx);
|
|
414
|
+
if (actualName !== serverDir) {
|
|
415
|
+
const found = windowByDir(serverDir);
|
|
416
|
+
if (found != null) winIdx = found;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (winIdx != null) {
|
|
420
|
+
tmux(['select-window', '-t', `${SESSION}:${winIdx}`]);
|
|
421
|
+
tmux(['send-keys', '-t', `${SESSION}:${winIdx}.0`, 'C-c']);
|
|
422
|
+
sleep(0.3);
|
|
423
|
+
tmux(['kill-window', '-t', `${SESSION}:${winIdx}`]);
|
|
424
|
+
console.error('Window removed.');
|
|
425
|
+
} else {
|
|
426
|
+
console.error(`Tmux window not found for ${serverDir} (cleaning up state file).`);
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
console.error("No tmux session '" + SESSION + "' found (cleaning up state file).");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fs.unlinkSync(statefile);
|
|
433
|
+
console.error('Done.');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Argument parsing
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
function main() {
|
|
441
|
+
let longMode = false;
|
|
442
|
+
const positional = [];
|
|
443
|
+
let dirIsForced = false;
|
|
444
|
+
|
|
445
|
+
for (const arg of process.argv.slice(2)) {
|
|
446
|
+
if (arg === '-l') {
|
|
447
|
+
longMode = true;
|
|
448
|
+
} else if (arg === '--version') {
|
|
449
|
+
console.log(version);
|
|
450
|
+
process.exit(0);
|
|
451
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
452
|
+
printHelp();
|
|
453
|
+
} else if (arg === '--') {
|
|
454
|
+
dirIsForced = true;
|
|
455
|
+
} else if (arg.startsWith('-')) {
|
|
456
|
+
if (arg === '--new' || arg === '--kill') {
|
|
457
|
+
console.error(`Error: '${arg}' is no longer a flag. Use 'ocmux ${arg.slice(2)}' instead.`);
|
|
458
|
+
} else {
|
|
459
|
+
console.error(`Error: unknown option '${arg}'`);
|
|
460
|
+
console.error(`Run '${SCRIPT} -h' for help.`);
|
|
461
|
+
}
|
|
462
|
+
process.exit(1);
|
|
463
|
+
} else {
|
|
464
|
+
positional.push(arg);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let subcommand = null;
|
|
469
|
+
let targetDir = null;
|
|
470
|
+
|
|
471
|
+
for (const arg of positional) {
|
|
472
|
+
if (dirIsForced) {
|
|
473
|
+
dirIsForced = false;
|
|
474
|
+
if (targetDir) {
|
|
475
|
+
console.error(`Error: unexpected argument '${arg}'`);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
targetDir = resolveDir(arg);
|
|
479
|
+
} else if (arg === 'new' || arg === 'kill' || arg === 'list') {
|
|
480
|
+
if (subcommand) {
|
|
481
|
+
console.error(`Error: multiple subcommands specified: '${subcommand}' and '${arg}'`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
subcommand = arg;
|
|
485
|
+
} else {
|
|
486
|
+
if (targetDir) {
|
|
487
|
+
console.error(`Error: unexpected argument '${arg}'`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
targetDir = resolveDir(arg);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!targetDir) targetDir = process.cwd();
|
|
495
|
+
|
|
496
|
+
// Dispatch
|
|
497
|
+
if (subcommand === 'list') {
|
|
498
|
+
if (targetDir !== process.cwd()) {
|
|
499
|
+
console.error("Error: 'list' does not accept a directory path.");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
cmdList();
|
|
503
|
+
} else if (subcommand === 'kill') {
|
|
504
|
+
const sf = findParentStatefile(targetDir);
|
|
505
|
+
if (!sf) {
|
|
506
|
+
console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
|
|
507
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
cmdKill(sf);
|
|
511
|
+
} else if (subcommand === 'new') {
|
|
512
|
+
cmdNew(targetDir, longMode);
|
|
513
|
+
} else {
|
|
514
|
+
// Default mode
|
|
515
|
+
const sf = findParentStatefile(targetDir);
|
|
516
|
+
if (!sf) {
|
|
517
|
+
console.error(`Error: no opencode server found in ${targetDir} or any of its parents.`);
|
|
518
|
+
console.error("Run 'ocmux new' to create a new server.");
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
ensureSession();
|
|
522
|
+
cmdSwitch(sf, longMode);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|