agentp 0.12.0 → 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 +36 -0
- package/bin/agentp +272 -83
- package/bin/tgagentp +57 -18
- package/package.json +1 -1
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
|
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
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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()
|
|
468
|
+
return title.toLowerCase() === sessionName.toLowerCase();
|
|
314
469
|
});
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
515
|
+
let answer;
|
|
351
516
|
try {
|
|
352
|
-
|
|
353
|
-
sessionId = session.id;
|
|
517
|
+
answer = await sendToSession(serverBase, sessionId, actualPromptText);
|
|
354
518
|
} catch (err) {
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
559
|
+
// Execute main processing with error handling for child mode
|
|
560
|
+
let output = '';
|
|
361
561
|
try {
|
|
362
|
-
|
|
562
|
+
output = await runMain();
|
|
363
563
|
} catch (err) {
|
|
364
|
-
|
|
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
|
-
//
|
|
369
|
-
let
|
|
370
|
-
if (
|
|
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
|
-
|
|
578
|
+
fs.writeFileSync(outputFile, output);
|
|
579
|
+
unlockFile(outputFile);
|
|
382
580
|
} catch (err) {
|
|
383
|
-
|
|
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(
|
|
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)
|
package/bin/tgagentp
CHANGED
|
@@ -1017,20 +1017,38 @@ async function processTelegramCommands(text, server, token, chatState) {
|
|
|
1017
1017
|
const filesSent = [];
|
|
1018
1018
|
const dir = serverDir(server);
|
|
1019
1019
|
|
|
1020
|
+
// Resolve a file path: absolute paths used directly, relative paths resolved
|
|
1021
|
+
// against the server's working directory. Returns null and sends an error
|
|
1022
|
+
// message if the resolved path is outside the server's working directory.
|
|
1023
|
+
function resolveUploadPath(p, errPrefix) {
|
|
1024
|
+
const absolute = path.isAbsolute(p) ? p : path.resolve(dir || '', p);
|
|
1025
|
+
if (!dir) return absolute; // no server dir — trust the path
|
|
1026
|
+
let realDir, realPath;
|
|
1027
|
+
try { realDir = fs.realpathSync(dir); } catch { realDir = dir; }
|
|
1028
|
+
try { realPath = fs.realpathSync(absolute); } catch { realPath = absolute; }
|
|
1029
|
+
if (!realPath.startsWith(realDir + path.sep) && realPath !== realDir) {
|
|
1030
|
+
sendLongMessage(token, chatState,
|
|
1031
|
+
`❌ ${errPrefix}: path outside project directory.\n` +
|
|
1032
|
+
` Path: \`${p}\`\n` +
|
|
1033
|
+
` Project: \`${realDir}\`\n\n` +
|
|
1034
|
+
`Use a full path (starting with /) or a path relative to the project root.`).catch(() => {});
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
return absolute;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1020
1040
|
for (let i = 0; i < lines.length; i++) {
|
|
1021
1041
|
const payload = parseTelegramLine(lines[i]);
|
|
1022
1042
|
if (!payload) continue;
|
|
1023
1043
|
|
|
1024
1044
|
if (payload.command === 'upload' && payload.path) {
|
|
1025
1045
|
toStrip.add(i);
|
|
1026
|
-
const resolved =
|
|
1027
|
-
if (
|
|
1028
|
-
log.error(`processTelegramCommands: path outside project rejected: ${payload.path}`);
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1046
|
+
const resolved = resolveUploadPath(payload.path, 'Upload');
|
|
1047
|
+
if (!resolved) continue;
|
|
1031
1048
|
try {
|
|
1032
1049
|
if (!fs.existsSync(resolved)) {
|
|
1033
|
-
|
|
1050
|
+
await sendLongMessage(token, chatState,
|
|
1051
|
+
`❌ File not found: \`${resolved}\``);
|
|
1034
1052
|
continue;
|
|
1035
1053
|
}
|
|
1036
1054
|
const fileData = fs.readFileSync(resolved);
|
|
@@ -1039,22 +1057,21 @@ async function processTelegramCommands(text, server, token, chatState) {
|
|
|
1039
1057
|
await sendTelegramDocument(token, chatState, fileData, fileName, caption);
|
|
1040
1058
|
filesSent.push(fileName);
|
|
1041
1059
|
} catch (err) {
|
|
1042
|
-
|
|
1060
|
+
await sendLongMessage(token, chatState,
|
|
1061
|
+
`❌ Failed to upload \`${resolved}\`: ${err.message}`);
|
|
1043
1062
|
}
|
|
1044
1063
|
} else if (payload.command === 'download' && payload.fileId && payload.path) {
|
|
1045
1064
|
toStrip.add(i);
|
|
1046
|
-
const resolved =
|
|
1047
|
-
if (
|
|
1048
|
-
log.error(`processTelegramCommands: download path outside project rejected: ${payload.path}`);
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1065
|
+
const resolved = resolveUploadPath(payload.path, 'Download');
|
|
1066
|
+
if (!resolved) continue;
|
|
1051
1067
|
try {
|
|
1052
1068
|
const fileData = await downloadTelegramFile(token, payload.fileId);
|
|
1053
1069
|
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
1054
1070
|
fs.writeFileSync(resolved, fileData);
|
|
1055
1071
|
filesSent.push(path.basename(resolved));
|
|
1056
1072
|
} catch (err) {
|
|
1057
|
-
|
|
1073
|
+
await sendLongMessage(token, chatState,
|
|
1074
|
+
`❌ Failed to download file to \`${resolved}\`: ${err.message}`);
|
|
1058
1075
|
}
|
|
1059
1076
|
} else if (payload.command === 'help') {
|
|
1060
1077
|
toStrip.add(i);
|
|
@@ -2154,8 +2171,13 @@ async function main() {
|
|
|
2154
2171
|
'Do NOT use CLI tools to generate them — just include them directly in your response:\n\n' +
|
|
2155
2172
|
'To send a file to the user:\n' +
|
|
2156
2173
|
'[Telegram]{"command":"upload","path":"<relative-path>","msg":"<optional caption>"}\n\n' +
|
|
2174
|
+
'**Important:** `path` is relative to the project root (the OpenCode server\'s working directory).\n' +
|
|
2175
|
+
'If the project uses a git worktree, the worktree directory IS the project root.\n' +
|
|
2176
|
+
'Use CLI commands like `cat ./path/to/file` to verify the file exists before uploading.\n\n' +
|
|
2157
2177
|
'To download a file the user sent:\n' +
|
|
2158
2178
|
'[Telegram]{"command":"download","fileId":"<id>","path":"<relative-destination>"}\n\n' +
|
|
2179
|
+
'**Important:** `path` is also relative to the project root (the OpenCode server\'s working directory).\n' +
|
|
2180
|
+
'Use `ls` or `pwd` to confirm the current working directory before specifying download paths.\n\n' +
|
|
2159
2181
|
'For more details about a command:\n' +
|
|
2160
2182
|
'[Telegram]{"command":"help","topic":"<upload|download|help>"}';
|
|
2161
2183
|
|
|
@@ -2174,6 +2196,7 @@ async function main() {
|
|
|
2174
2196
|
const monRef = { current: null };
|
|
2175
2197
|
st._monitorRef = monRef;
|
|
2176
2198
|
st._monitorStarted = true;
|
|
2199
|
+
st._monitoredSessionId = sessionId;
|
|
2177
2200
|
|
|
2178
2201
|
const notifiedPerms = new Set();
|
|
2179
2202
|
|
|
@@ -2239,6 +2262,7 @@ async function main() {
|
|
|
2239
2262
|
if (getState(server)._monitorRef === monRef) {
|
|
2240
2263
|
getState(server)._monitorRef = null;
|
|
2241
2264
|
getState(server)._monitorStarted = false;
|
|
2265
|
+
getState(server)._monitoredSessionId = null;
|
|
2242
2266
|
}
|
|
2243
2267
|
});
|
|
2244
2268
|
}
|
|
@@ -2271,6 +2295,20 @@ async function main() {
|
|
|
2271
2295
|
|
|
2272
2296
|
async function processMessageAsync(chatId, text, url, useSession, sessionId, myTaskId, replyTo, threadId) {
|
|
2273
2297
|
const st = getState(url);
|
|
2298
|
+
// Auto-discover active session if not set
|
|
2299
|
+
if (!sessionId) {
|
|
2300
|
+
try {
|
|
2301
|
+
const discovered = await discoverActiveSession(url);
|
|
2302
|
+
if (discovered) {
|
|
2303
|
+
st.activeSessionId = discovered.id;
|
|
2304
|
+
st.activeSessionName = discovered.name;
|
|
2305
|
+
sessionId = discovered.id;
|
|
2306
|
+
useSession = true;
|
|
2307
|
+
}
|
|
2308
|
+
} catch (err) {
|
|
2309
|
+
log.debug(`processMessageAsync: auto-discover error: ${err.message}`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2274
2312
|
const isCancelled = () => getState(url).taskId !== myTaskId;
|
|
2275
2313
|
const cancelRef = { current: null };
|
|
2276
2314
|
const chatState = { chatId, threadId };
|
|
@@ -2281,11 +2319,12 @@ async function main() {
|
|
|
2281
2319
|
st._cancelRef = cancelRef;
|
|
2282
2320
|
st._sessionReq = sessionCancelRef;
|
|
2283
2321
|
|
|
2284
|
-
// Ensure the persistent session monitor is running
|
|
2285
|
-
//
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2322
|
+
// Ensure the persistent session monitor is running and tracking the current session.
|
|
2323
|
+
// Always restart when sessionId changes so TUI-initiated session switches are followed.
|
|
2324
|
+
if (useSession && sessionId) {
|
|
2325
|
+
if (!st._monitorStarted || st._monitoredSessionId !== sessionId) {
|
|
2326
|
+
startSessionMonitor(url, sessionId);
|
|
2327
|
+
}
|
|
2289
2328
|
}
|
|
2290
2329
|
|
|
2291
2330
|
try {
|