agentp 0.11.11 → 1.12.1

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/README.md CHANGED
@@ -22,6 +22,8 @@ It is designed for prompt-driven workflows where you want to do things like:
22
22
 
23
23
  These tools are built for my own daily workflow. They are heavily AI-assisted — including the tests — and I review things before shipping, but the real test is using them every day. Bugs happen; I value a working feature more than a flawless one. MIT license, no warranty. Issues, suggestions, and PRs are welcome.
24
24
 
25
+ **Stability note:** `agentp` and `ocmux` are stable and used daily. `tgagentp` is still in a highly experimental stage — expect breaking changes and occasional bugs.
26
+
25
27
  ## Install
26
28
 
27
29
  From npm:
@@ -57,6 +59,7 @@ agentp [options] [url]
57
59
  Options:
58
60
 
59
61
  - `--qa`: print the original prompt and answer with labels (useful when used as a filter)
62
+ - `--defer`: deferred execution — submit prompt, get immediate ticket, retrieve result later
60
63
  - `--tg`: forward the answer to Telegram via tgagentp gateway (error if unreachable)
61
64
  - `--no-tg`: do not forward to Telegram
62
65
  - `--flush`: flush tgagentp's recorded buffer without prepending it to output
@@ -142,6 +145,39 @@ From Vim/Neovim, flush the recorded buffer without prepending context:
142
145
  Useful when you've finished a conversation thread and want to reset the recorded
143
146
  context for a new topic.
144
147
 
148
+ Deferred execution with `--defer`:
149
+
150
+ Submit a prompt and get an immediate ticket to retrieve the result later:
151
+
152
+ ```bash
153
+ # Submit a prompt and get a deferred reference
154
+ DEFERRED=$(printf "Refactor the authentication module" | agentp --defer)
155
+ # Output: <agentp-deferred>/tmp/agentp_deferred_20260803_1430_a1b2.tmp</agentp-deferred>
156
+
157
+ # Continue working... retrieve the result when ready
158
+ printf '%s\n' "$DEFERRED" | agentp --defer
159
+ # Output: (the agent's response)
160
+ ```
161
+
162
+ Works as a Vim/Neovim filter with deferred execution:
163
+
164
+ ```vim
165
+ " Submit selection, get ticket immediately, continue editing
166
+ :'<,'>!agentp --defer --qa
167
+
168
+ " Later, retrieve the result
169
+ :r !printf '%s\n' "<agentp-deferred>/tmp/agentp_deferred_...tmp</agentp-deferred>" | agentp --defer
170
+ ```
171
+
172
+ The deferred workflow:
173
+ 1. Submit prompt with `--defer` → get immediate ticket (`<agentp-deferred>...</agentp-deferred>`)
174
+ 2. Continue working (the agent processes in background)
175
+ 3. When ready, pipe the ticket back to `agentp --defer` to retrieve the result
176
+ 4. If the agent is still processing, you get your input back (identity filter)
177
+ 4. If complete, you get the agent's response and the temp file is cleaned up
178
+
179
+ Useful for long-running tasks where you don't want to block your editor.
180
+
145
181
  Retrieve the last 3 assistant answers from session history:
146
182
 
147
183
  ```bash
@@ -231,21 +267,28 @@ Non-text Telegram updates (photos, stickers, etc.) are silently ignored.
231
267
  ### Slash commands
232
268
 
233
269
  | Command | Action |
234
- |---|---|
270
+ |---|---|---|
235
271
  | `/help [topic]` | Show general help or help for a topic (`servers`, `sessions`, `agents`, `models`, `allow`, `think`, `record`, `queue`) |
236
272
  | `/servers` | List all ocmux-served projects (▶ active, 🔌 disconnected, 💀 dead) |
237
- | `/servers switch <name> [--force]` | Switch active server; matches by full path, basename, or substring; `--force` takes over from another chat |
273
+ | `/server <name>` | Switch active server; matches by full path, basename, or substring |
274
+ | `/server --force <name>` | Take over a server from another chat |
238
275
  | `/resurrect [path]` | Restart a crashed server from its `.ocmux.json`; accepts optional directory path |
276
+ | `/sessions` | List sessions (numbered, newest first) |
277
+ | `/session <name-or-number>` | Switch to a session by name or position |
278
+ | `/session new [name]` | Create a new session |
279
+ | `/session rename <name>` | Rename the active session |
280
+ | `/agents` | List available agents/models |
281
+ | `/agent <name>` | Switch the active agent for subsequent messages (synced to server via API) |
282
+ | `/models` | List providers and models (▶ marks current) |
283
+ | `/model <providerID/modelID>` | Switch session model |
239
284
  | `/allow` | Approve a permission request once |
240
285
  | `/reject` | Deny a permission request |
241
286
  | `/always` | Approve and remember for the session |
242
287
  | `/answer <number>` | Respond to a question asked by the AI (structured multiple-choice) |
243
288
  | `/markdown` | Send the original markdown of the last response as a `.md` file; reply to a message to get that specific response |
244
- | `/agents list` | List available agents/models |
245
- | `/agents switch <name>` | Switch the active agent for subsequent messages (synced to server via API) |
246
289
  | `/shutdown [force\|clear]` | (requires `--dev`) Stop tgagentp; `clear` also wipes saved connections |
247
290
 
248
- > **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agents switch <name>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
291
+ > **Note on model/agent switching:** Changing the model in the OpenCode TUI's prompt dropdown is a **local UI action** — it only takes effect server-side after a message is sent *through the TUI*. If you change the model in the TUI and then send a prompt via `agentp` or Telegram, the old model will still be used. Use `/agent <name>` or `/model <providerID/modelID>` from Telegram (or `tgagentp`) to change models — this explicitly calls the API and syncs correctly.
249
292
 
250
293
  #### TUI Command Passthrough
251
294
 
@@ -255,7 +298,7 @@ Messages starting with `//` are forwarded to the OpenCode TUI as raw keystrokes
255
298
 
256
299
  ### Chat-server ownership
257
300
 
258
- Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/servers switch <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
301
+ Each server can be owned by at most one chat at a time. New chats start disconnected. Use `/server <name>` to connect; `--force` takes over and notifies the previous owner. Connections are persisted to `/tmp/tgagentp-connections.json` and restored automatically on restart (server URL is re-discovered from `.ocmux.json`).
259
302
 
260
303
  ### Per-server state
261
304
 
@@ -287,7 +330,7 @@ tgagentp starts a tiny HTTP server on `127.0.0.1` that accepts `POST /send` requ
287
330
  - Port is written to `/tmp/tgagentp-port` for agentp discovery.
288
331
  - Authentication reuses `OPENCODE_SERVER_PASSWORD`.
289
332
  - Messages for the owning chat's active server are delivered immediately.
290
- - Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/servers switch`.
333
+ - Messages for non-active servers are queued per-server with debounced notifications (configurable via `TGAGENTP_DEBOUNCE_MS`); delivered on `/server <name>`.
291
334
  - Server health detection pre-sends: if a server is unreachable, messages are auto-queued and delivered when it comes back. `/flush` clears all queues.
292
335
  - When [/record](#tgagentp) is active, the gateway response includes the recorded conversation buffer. `agentp --qa` prepends this buffer (with rulers) to its stdout so the full Telegram context is available to OpenCode. Use `agentp --qa --flush` to flush the buffer without prepending.
293
336
 
package/bin/agentp CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  const http = require('http');
4
4
  const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const crypto = require('crypto');
5
8
  const readline = require('readline');
6
9
  const child_process = require('child_process');
7
10
  const { version } = require('../package.json');
@@ -60,6 +63,51 @@ function notifyAgentpGateway(port, server, text) {
60
63
  });
61
64
  }
62
65
 
66
+ function isFileLocked(filePath) {
67
+ const lockPath = filePath + '.lock';
68
+ try {
69
+ if (!fs.existsSync(lockPath)) return false;
70
+ const pid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
71
+ if (isNaN(pid)) return false;
72
+ // Check if process is still alive
73
+ try {
74
+ process.kill(pid, 0);
75
+ return true; // Process exists, file is locked
76
+ } catch (err) {
77
+ // Process doesn't exist, lock is stale
78
+ fs.unlinkSync(lockPath);
79
+ return false;
80
+ }
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ function lockFile(filePath) {
87
+ const lockPath = filePath + '.lock';
88
+ fs.writeFileSync(lockPath, String(process.pid));
89
+ }
90
+
91
+ function unlockFile(filePath) {
92
+ const lockPath = filePath + '.lock';
93
+ try {
94
+ fs.unlinkSync(lockPath);
95
+ } catch {
96
+ // Ignore errors
97
+ }
98
+ }
99
+
100
+ function generateTempFilename() {
101
+ const now = new Date();
102
+ const timestamp = now.getFullYear().toString() +
103
+ String(now.getMonth() + 1).padStart(2, '0') +
104
+ String(now.getDate()).padStart(2, '0') + '_' +
105
+ String(now.getHours()).padStart(2, '0') +
106
+ String(now.getMinutes()).padStart(2, '0');
107
+ const hash = crypto.randomBytes(2).toString('hex').slice(0, 4);
108
+ return path.join(os.tmpdir(), `agentp_deferred_${timestamp}_${hash}.tmp`);
109
+ }
110
+
63
111
  async function readAllStdin() {
64
112
  let text = '';
65
113
  const rl = readline.createInterface({ input: process.stdin });
@@ -78,6 +126,10 @@ async function main() {
78
126
  let serverBase = 'http://localhost:4096';
79
127
  let qaMode = false;
80
128
  let flushMode = false;
129
+ let deferMode = false;
130
+ let deferChildMode = false;
131
+ let promptFile = null;
132
+ let outputFile = null;
81
133
  let sessionName = null;
82
134
  let newSession = false;
83
135
  // tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
@@ -96,6 +148,7 @@ async function main() {
96
148
  console.error('Options:');
97
149
  console.error(' --version Show version');
98
150
  console.error(' --qa Print the original prompt and answer with labels (useful as a filter)');
151
+ console.error(' --defer Deferred execution: write prompt to temp file, return path immediately');
99
152
  console.error(' --tg Forward the answer to Telegram via agentp gateway (error if unavailable)');
100
153
  console.error(' --no-tg Do not forward to Telegram');
101
154
  console.error(' --flush Flush tgagentp\'s recorded buffer without prepending it to output');
@@ -139,6 +192,16 @@ async function main() {
139
192
  process.exit(0);
140
193
  } else if (args[i] === '--qa') {
141
194
  qaMode = true;
195
+ } else if (args[i] === '--defer') {
196
+ deferMode = true;
197
+ } else if (args[i] === '--defer-child') {
198
+ deferChildMode = true;
199
+ } else if (args[i] === '--prompt-file') {
200
+ i++;
201
+ promptFile = args[i];
202
+ } else if (args[i] === '--output-file') {
203
+ i++;
204
+ outputFile = args[i];
142
205
  } else if (args[i] === '--flush') {
143
206
  flushMode = true;
144
207
  } else if (args[i] === '--session') {
@@ -289,113 +352,239 @@ async function main() {
289
352
 
290
353
  const promptText = await readAllStdin();
291
354
 
292
- // Find most recent session or create one
293
- let sessions;
294
- try {
295
- sessions = await listSessions(serverBase);
296
- } catch (err) {
297
- console.error('Error connecting to server:', err.message);
298
- process.exit(1);
355
+ // Handle --defer mode
356
+ if (deferMode) {
357
+ // Check if stdin is a deferred reference
358
+ const deferredMatch = promptText.match(/<agentp-deferred>(.*?)<\/agentp-deferred>/);
359
+
360
+ if (deferredMatch) {
361
+ // This is a deferred reference - retrieve the result
362
+ const deferredPath = deferredMatch[1].trim();
363
+
364
+ if (!fs.existsSync(deferredPath)) {
365
+ // File doesn't exist - error
366
+ process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
367
+ process.exit(1);
368
+ }
369
+
370
+ // Check if file is locked (still processing) or empty (not ready yet)
371
+ const isLocked = isFileLocked(deferredPath);
372
+ let content = '';
373
+ try {
374
+ content = fs.readFileSync(deferredPath, 'utf8');
375
+ } catch {
376
+ content = '';
377
+ }
378
+
379
+ if (isLocked || content === '') {
380
+ // File is locked (still processing) or empty (not ready yet) - identity filter
381
+ process.stdout.write(promptText);
382
+ process.exit(0);
383
+ }
384
+
385
+ // File exists, not locked, and has content - read and delete
386
+ try {
387
+ fs.unlinkSync(deferredPath);
388
+ process.stdout.write(content);
389
+ process.exit(0);
390
+ } catch (err) {
391
+ process.stdout.write(`Error reading deferred file: ${err.message}\n`);
392
+ process.exit(1);
393
+ }
394
+ } else {
395
+ // This is a regular prompt - create deferred execution
396
+ const tempPath = generateTempFilename();
397
+ const promptTempPath = tempPath + '.prompt';
398
+
399
+ // Write prompt to temp file for child process
400
+ fs.writeFileSync(promptTempPath, promptText);
401
+
402
+ // Create and lock the output file
403
+ fs.writeFileSync(tempPath, '');
404
+ lockFile(tempPath);
405
+
406
+ // Output the deferred reference
407
+ process.stdout.write(`<agentp-deferred>${tempPath}</agentp-deferred>\n`);
408
+
409
+ // Spawn detached child process
410
+ const childArgs = [
411
+ __filename,
412
+ '--defer-child',
413
+ '--prompt-file', promptTempPath,
414
+ '--output-file', tempPath,
415
+ ];
416
+
417
+ // Pass through other flags
418
+ if (qaMode) childArgs.push('--qa');
419
+ if (tgMode === true) childArgs.push('--tg');
420
+ if (tgMode === false) childArgs.push('--no-tg');
421
+ if (flushMode) childArgs.push('--flush');
422
+ if (sessionName) {
423
+ childArgs.push('--session', sessionName);
424
+ if (newSession) childArgs.push('--new');
425
+ }
426
+ childArgs.push(serverBase);
427
+
428
+ const child = child_process.spawn(process.execPath, childArgs, {
429
+ detached: true,
430
+ stdio: 'ignore',
431
+ });
432
+ child.unref();
433
+
434
+ process.exit(0);
435
+ }
299
436
  }
300
437
 
301
- let sessionId = null;
438
+ // Handle --defer-child mode (child process)
439
+ let actualPromptText = promptText;
440
+ if (deferChildMode && promptFile) {
441
+ try {
442
+ actualPromptText = fs.readFileSync(promptFile, 'utf8');
443
+ fs.unlinkSync(promptFile);
444
+ } catch (err) {
445
+ const errMsg = `Error reading prompt file: ${err.message}\n`;
446
+ if (outputFile) { fs.writeFileSync(outputFile, errMsg); unlockFile(outputFile); }
447
+ process.stdout.write(errMsg);
448
+ process.exit(1);
449
+ }
450
+ }
302
451
 
303
- if (sessionName) {
304
- // Find session by name
305
- const matches = sessions.filter(s => {
306
- const title = s.title || s.slug || s.id;
307
- return title.toLowerCase() === sessionName.toLowerCase();
308
- });
309
- if (matches.length === 0) {
310
- // Try partial match
311
- const partial = sessions.filter(s => {
452
+ // Wrap main processing in try-catch for deferChildMode to write errors to output file
453
+ async function runMain() {
454
+ // Find most recent session or create one
455
+ let sessions;
456
+ try {
457
+ sessions = await listSessions(serverBase);
458
+ } catch (err) {
459
+ throw new Error(`Error connecting to server: ${err.message}`);
460
+ }
461
+
462
+ let sessionId = null;
463
+
464
+ if (sessionName) {
465
+ // Find session by name
466
+ const matches = sessions.filter(s => {
312
467
  const title = s.title || s.slug || s.id;
313
- return title.toLowerCase().includes(sessionName.toLowerCase());
468
+ return title.toLowerCase() === sessionName.toLowerCase();
314
469
  });
315
- if (partial.length === 1) {
316
- sessionId = partial[0].id;
317
- } else if (partial.length > 1) {
318
- console.error(`Error: multiple sessions match "${sessionName}":`);
319
- partial.forEach(s => console.error(` - ${s.title || s.slug || s.id}`));
320
- process.exit(1);
321
- } else if (newSession) {
322
- // Create new session
323
- try {
324
- const session = await createSession(serverBase, sessionName);
325
- sessionId = session.id;
326
- await selectSession(serverBase, sessionId);
327
- } catch (err) {
328
- console.error('Error creating session:', err.message);
329
- process.exit(1);
470
+ if (matches.length === 0) {
471
+ // Try partial match
472
+ const partial = sessions.filter(s => {
473
+ const title = s.title || s.slug || s.id;
474
+ return title.toLowerCase().includes(sessionName.toLowerCase());
475
+ });
476
+ if (partial.length === 1) {
477
+ sessionId = partial[0].id;
478
+ } else if (partial.length > 1) {
479
+ throw new Error(`Error: multiple sessions match "${sessionName}"`);
480
+ } else if (newSession) {
481
+ // Create new session
482
+ try {
483
+ const session = await createSession(serverBase, sessionName);
484
+ sessionId = session.id;
485
+ await selectSession(serverBase, sessionId);
486
+ } catch (err) {
487
+ throw new Error(`Error creating session: ${err.message}`);
488
+ }
489
+ } else {
490
+ throw new Error(`Error: no session found matching "${sessionName}"`);
330
491
  }
331
492
  } else {
332
- console.error(`Error: no session found matching "${sessionName}"`);
333
- console.error('Use --new to create a new session with this name');
334
- process.exit(1);
493
+ sessionId = matches[0].id;
494
+ }
495
+ } else if (sessions && sessions.length > 0) {
496
+ const sorted = sessions
497
+ .filter(s => s.time && (s.time.updated || s.time.created))
498
+ .sort((a, b) => {
499
+ const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
500
+ const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
501
+ return bTime - aTime;
502
+ });
503
+ if (sorted.length > 0) sessionId = sorted[0].id;
504
+ }
505
+
506
+ if (!sessionId) {
507
+ try {
508
+ const session = await createSession(serverBase, 'agentp');
509
+ sessionId = session.id;
510
+ } catch (err) {
511
+ throw new Error(`Error creating session: ${err.message}`);
335
512
  }
336
- } else {
337
- sessionId = matches[0].id;
338
513
  }
339
- } else if (sessions && sessions.length > 0) {
340
- const sorted = sessions
341
- .filter(s => s.time && (s.time.updated || s.time.created))
342
- .sort((a, b) => {
343
- const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
344
- const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
345
- return bTime - aTime;
346
- });
347
- if (sorted.length > 0) sessionId = sorted[0].id;
348
- }
349
514
 
350
- if (!sessionId) {
515
+ let answer;
351
516
  try {
352
- const session = await createSession(serverBase, 'agentp');
353
- sessionId = session.id;
517
+ answer = await sendToSession(serverBase, sessionId, actualPromptText);
354
518
  } catch (err) {
355
- console.error('Error creating session:', err.message);
356
- process.exit(1);
519
+ throw new Error(`Error sending prompt: ${err.message}`);
520
+ }
521
+
522
+ // Build the message to send to Telegram (QA pair with rulers)
523
+ let tgMessage = answer;
524
+ if (qaMode) {
525
+ tgMessage = HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
526
+ if (!answer.endsWith('\n')) tgMessage += '\n';
527
+ tgMessage += FINAL_ROW;
357
528
  }
529
+
530
+ // Post-send: notify tgagentp, get recorded context back
531
+ let buffered = [];
532
+ let tgError = null;
533
+ if (tgPort) {
534
+ try {
535
+ buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
536
+ } catch (err) {
537
+ tgError = err;
538
+ }
539
+ }
540
+
541
+ // Build output (prepend recorded context if any, unless --flush)
542
+ let output = '';
543
+ if (qaMode) {
544
+ if (!flushMode && buffered.length > 0) {
545
+ for (const msg of buffered) {
546
+ const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
547
+ output += row + msg.text + '\n';
548
+ }
549
+ }
550
+ output += tgMessage;
551
+ } else {
552
+ output = tgMessage;
553
+ if (!tgMessage.endsWith('\n')) output += '\n';
554
+ }
555
+
556
+ return output;
358
557
  }
359
558
 
360
- let answer;
559
+ // Execute main processing with error handling for child mode
560
+ let output = '';
361
561
  try {
362
- answer = await sendToSession(serverBase, sessionId, promptText);
562
+ output = await runMain();
363
563
  } catch (err) {
364
- console.error('Error sending prompt:', err.message);
564
+ const errMsg = err.message + '\n';
565
+ if (deferChildMode && outputFile) {
566
+ fs.writeFileSync(outputFile, errMsg);
567
+ unlockFile(outputFile);
568
+ } else {
569
+ process.stdout.write(errMsg);
570
+ }
365
571
  process.exit(1);
366
572
  }
367
573
 
368
- // Build the message to send to Telegram (QA pair with rulers)
369
- let tgMessage = answer;
370
- if (qaMode) {
371
- tgMessage = HUMAN_ROW + promptText + AGENT_ROW + answer;
372
- if (!answer.endsWith('\n')) tgMessage += '\n';
373
- tgMessage += FINAL_ROW;
374
- }
375
-
376
- // Post-send: notify tgagentp, get recorded context back
377
- let buffered = [];
378
- let tgError = null;
379
- if (tgPort) {
574
+ // Write output to file (child mode) or stdout (normal mode)
575
+ let finalOutput = output;
576
+ if (deferChildMode && outputFile) {
380
577
  try {
381
- buffered = await notifyAgentpGateway(tgPort, serverBase, tgMessage);
578
+ fs.writeFileSync(outputFile, output);
579
+ unlockFile(outputFile);
382
580
  } catch (err) {
383
- tgError = err;
384
- }
385
- }
386
-
387
- // Build stdout output (prepend recorded context if any, unless --flush)
388
- if (qaMode) {
389
- if (!flushMode && buffered.length > 0) {
390
- for (const msg of buffered) {
391
- const row = msg.role === 'user' ? HUMAN_ROW : AGENT_ROW;
392
- process.stdout.write(row + msg.text + '\n');
393
- }
581
+ const errMsg = `Error writing output file: ${err.message}\n`;
582
+ try { fs.writeFileSync(outputFile, errMsg); } catch {}
583
+ unlockFile(outputFile);
584
+ process.exit(1);
394
585
  }
395
- process.stdout.write(tgMessage);
396
586
  } else {
397
- process.stdout.write(tgMessage);
398
- if (!tgMessage.endsWith('\n')) process.stdout.write('\n');
587
+ process.stdout.write(output);
399
588
  }
400
589
 
401
590
  // Warnings at the end (stderr)