agentp 1.14.0 ā 2.0.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/CONTRIBUTING.md +80 -83
- package/README.md +165 -51
- package/bin/agentp +525 -84
- package/bin/ocmux +1376 -580
- package/docs/specification.md +12 -454
- package/docs/specification_v2.md +652 -0
- package/lib/ocmux.js +123 -197
- package/lib/opencode.js +363 -657
- package/lib/project-state.js +213 -0
- package/package.json +1 -1
package/bin/agentp
CHANGED
|
@@ -13,9 +13,15 @@ const {
|
|
|
13
13
|
sendToSessionAsync,
|
|
14
14
|
listSessions,
|
|
15
15
|
createSession,
|
|
16
|
+
createSessionWithModel,
|
|
16
17
|
getSession,
|
|
17
18
|
selectSession,
|
|
19
|
+
sortSessionsByRecency,
|
|
20
|
+
interruptSession,
|
|
21
|
+
getActiveSessions,
|
|
18
22
|
} = require('../lib/opencode');
|
|
23
|
+
const projectState = require('../lib/project-state');
|
|
24
|
+
const ocmuxLib = require('../lib/ocmux');
|
|
19
25
|
|
|
20
26
|
const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
|
|
21
27
|
|
|
@@ -163,14 +169,19 @@ function parseDeferredTicketInput(text) {
|
|
|
163
169
|
if (!data || typeof data.path !== 'string' || !data.path) return null;
|
|
164
170
|
const rest = afterPrefix.slice(jsonEnd);
|
|
165
171
|
const followupText = rest.trim() ? stripFollowupRow(rest.trimStart()) : '';
|
|
166
|
-
|
|
172
|
+
const ticket = {
|
|
167
173
|
ctime: typeof data.ctime === 'string' ? data.ctime : null,
|
|
168
174
|
path: data.path,
|
|
169
175
|
defer: typeof data.defer === 'number' && data.defer > 0 ? data.defer : 0,
|
|
170
176
|
server: typeof data.server === 'string' ? data.server : null,
|
|
171
177
|
sessionId: typeof data.sessionId === 'string' ? data.sessionId : null,
|
|
178
|
+
cancelled: data.cancelled === true,
|
|
172
179
|
followupText,
|
|
173
180
|
};
|
|
181
|
+
if (Array.isArray(data.sessionIds)) {
|
|
182
|
+
ticket.sessionIds = data.sessionIds.filter(x => typeof x === 'string' && x);
|
|
183
|
+
}
|
|
184
|
+
return ticket;
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
function parseDeferredTicket(text) {
|
|
@@ -187,20 +198,58 @@ function formatTicket(ticket, elapsed, compact) {
|
|
|
187
198
|
const fields = { ctime: ticket.ctime, path: ticket.path };
|
|
188
199
|
if (ticket.server) fields.server = ticket.server;
|
|
189
200
|
if (ticket.sessionId) fields.sessionId = ticket.sessionId;
|
|
201
|
+
if (Array.isArray(ticket.sessionIds) && ticket.sessionIds.length > 0) fields.sessionIds = ticket.sessionIds;
|
|
190
202
|
if (elapsed !== undefined) fields.elapsed = elapsed;
|
|
191
203
|
if (ticket.defer > 0) fields.defer = ticket.defer;
|
|
204
|
+
// Always present so it can be flipped to true and piped back to cancel.
|
|
205
|
+
fields.cancelled = ticket.cancelled === true;
|
|
192
206
|
return 'agentp_ticket ' + (compact ? JSON.stringify(fields) : JSON.stringify(fields, null, 2));
|
|
193
207
|
}
|
|
194
208
|
|
|
195
|
-
|
|
209
|
+
// Cancel a deferred ticket: interrupt the session (ESC-equivalent) and discard
|
|
210
|
+
// the ticket. Prints a message and exits 0.
|
|
211
|
+
async function cancelTicket(ticket) {
|
|
212
|
+
let interrupted = false;
|
|
213
|
+
const ids = Array.isArray(ticket.sessionIds) && ticket.sessionIds.length > 0
|
|
214
|
+
? ticket.sessionIds
|
|
215
|
+
: (ticket.sessionId ? [ticket.sessionId] : []);
|
|
216
|
+
if (ticket.server && ids.length > 0) {
|
|
217
|
+
const interruptedAll = await Promise.all(ids.map(id => interruptSession(ticket.server, id)));
|
|
218
|
+
interrupted = interruptedAll.some(Boolean);
|
|
219
|
+
}
|
|
220
|
+
try { fs.unlinkSync(ticket.path); } catch {}
|
|
221
|
+
try { fs.unlinkSync(ticket.path + '.lock'); } catch {}
|
|
222
|
+
removeQueuedFollowups(ticket.path);
|
|
223
|
+
process.stdout.write(`\nš« Prompt cancelled${interrupted ? '' : ' (or already finished)'}.\n`);
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Print a deferred ticket (with elapsed) and exit ā used for the interrupt path.
|
|
228
|
+
function printTicketAndExit(ticket, onlineTicket) {
|
|
229
|
+
const elapsed = ticket && ticket.ctime
|
|
230
|
+
? Math.max(0, Math.floor((Date.now() - Date.parse(ticket.ctime)) / 1000))
|
|
231
|
+
: 0;
|
|
232
|
+
process.stdout.write(formatTicket(ticket, elapsed, onlineTicket) + '\n');
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function resolveTargetSession(serverBase, sessionName, newSession, opts = {}) {
|
|
237
|
+
// opts: { directory, preferredSession } ā v2 project/session resolution from
|
|
238
|
+
// the nearest .ocmux.json. `directory` scopes the listing to the project;
|
|
239
|
+
// `preferredSession` is a stored session ID validated against that listing.
|
|
240
|
+
const directory = opts.directory || undefined;
|
|
241
|
+
const preferredSession = opts.preferredSession || null;
|
|
242
|
+
|
|
196
243
|
let sessions;
|
|
197
244
|
try {
|
|
198
|
-
sessions = await listSessions(serverBase);
|
|
245
|
+
sessions = await listSessions(serverBase, directory);
|
|
199
246
|
} catch (err) {
|
|
200
247
|
throw new Error(`Error connecting to server: ${err.message}`);
|
|
201
248
|
}
|
|
202
249
|
|
|
203
250
|
if (sessionName) {
|
|
251
|
+
const byId = sessions.find(s => String(s.id) === sessionName);
|
|
252
|
+
if (byId) return byId.id;
|
|
204
253
|
const matches = sessions.filter(s => {
|
|
205
254
|
const title = s.title || s.slug || s.id;
|
|
206
255
|
return title.toLowerCase() === sessionName.toLowerCase();
|
|
@@ -214,7 +263,7 @@ async function resolveTargetSession(serverBase, sessionName, newSession) {
|
|
|
214
263
|
if (partial.length > 1) throw new Error(`Error: multiple sessions match "${sessionName}"`);
|
|
215
264
|
if (!newSession) throw new Error(`Error: no session found matching "${sessionName}"`);
|
|
216
265
|
try {
|
|
217
|
-
const session = await
|
|
266
|
+
const session = await createSessionWithModel(serverBase, sessionName, directory, sessions);
|
|
218
267
|
await selectSession(serverBase, session.id);
|
|
219
268
|
return session.id;
|
|
220
269
|
} catch (err) {
|
|
@@ -224,19 +273,20 @@ async function resolveTargetSession(serverBase, sessionName, newSession) {
|
|
|
224
273
|
return matches[0].id;
|
|
225
274
|
}
|
|
226
275
|
|
|
276
|
+
// Prefer the session recorded in .ocmux.json when it still exists.
|
|
277
|
+
if (preferredSession && sessions.some(s => String(s.id) === String(preferredSession))) {
|
|
278
|
+
return preferredSession;
|
|
279
|
+
}
|
|
280
|
+
|
|
227
281
|
if (sessions && sessions.length > 0) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
|
|
232
|
-
const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
|
|
233
|
-
return bTime - aTime;
|
|
234
|
-
});
|
|
282
|
+
// Only "main" sessions (no parentID) are considered as implicit targets ā
|
|
283
|
+
// child/subagent sessions are work products, not conversation targets.
|
|
284
|
+
const sorted = sortSessionsByRecency(sessions).filter(s => s.id && !s.parentID);
|
|
235
285
|
if (sorted.length > 0) return sorted[0].id;
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
try {
|
|
239
|
-
const session = await
|
|
289
|
+
const session = await createSessionWithModel(serverBase, 'agentp', directory, sessions);
|
|
240
290
|
return session.id;
|
|
241
291
|
} catch (err) {
|
|
242
292
|
throw new Error(`Error creating session: ${err.message}`);
|
|
@@ -273,6 +323,41 @@ function sleepMs(ms) {
|
|
|
273
323
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
274
324
|
}
|
|
275
325
|
|
|
326
|
+
// Wait indefinitely for a deferred result. Resolves with `{ result }` once the
|
|
327
|
+
// answer is ready, or `{ interrupted: true }` if SIGINT is received while
|
|
328
|
+
// waiting (any detached child keeps running). The caller decides how to react
|
|
329
|
+
// to the interrupt (normally print a ticket and exit).
|
|
330
|
+
function waitForDeferredResultForever(filePath) {
|
|
331
|
+
return new Promise((resolve) => {
|
|
332
|
+
let timer = null;
|
|
333
|
+
let done = false;
|
|
334
|
+
|
|
335
|
+
const finish = (value) => {
|
|
336
|
+
if (done) return;
|
|
337
|
+
done = true;
|
|
338
|
+
if (timer) clearTimeout(timer);
|
|
339
|
+
process.removeListener('SIGINT', onSigint);
|
|
340
|
+
resolve(value);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
function onSigint() {
|
|
344
|
+
finish({ interrupted: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const poll = () => {
|
|
348
|
+
const result = readDeferredResult(filePath);
|
|
349
|
+
if (result !== null) {
|
|
350
|
+
finish({ result });
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
timer = setTimeout(poll, DEFER_POLL_MS);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
process.on('SIGINT', onSigint);
|
|
357
|
+
poll();
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
276
361
|
function appendUnusedFollowup(result, followupText) {
|
|
277
362
|
if (!followupText) return result;
|
|
278
363
|
return result + (result.endsWith('\n') ? '' : '\n') + followupText;
|
|
@@ -313,10 +398,166 @@ function injectQueuedFollowups(result, followups) {
|
|
|
313
398
|
return before + (before.endsWith('\n') ? '' : '\n') + addition + after;
|
|
314
399
|
}
|
|
315
400
|
|
|
316
|
-
function formatDeferredResult(filePath, result, qaMode, unusedFollowupText) {
|
|
401
|
+
function formatDeferredResult(filePath, result, qaMode, unusedFollowupText, header) {
|
|
317
402
|
const followups = qaMode ? readQueuedFollowups(filePath) : [];
|
|
318
403
|
const output = qaMode ? injectQueuedFollowups(result, followups) : result;
|
|
319
|
-
|
|
404
|
+
// The stored answer (produced by the detached child) may already carry the
|
|
405
|
+
// project/session header ā don't prepend it a second time on retrieval.
|
|
406
|
+
const withHeader = qaMode && header && !carriesHeader(output) ? header + output : output;
|
|
407
|
+
return appendUnusedFollowup(withHeader, unusedFollowupText);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Does the text already begin with the š / š¬ header lines?
|
|
411
|
+
function carriesHeader(text) {
|
|
412
|
+
return /^(?:\u{1F4C2}|\u{1F4AC})/u.test(String(text || '').trimStart());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Output a ready deferred result, clean up its temp files, and exit 0.
|
|
416
|
+
function outputReadyResult(filePath, result, qaMode, unusedFollowupText, header) {
|
|
417
|
+
try {
|
|
418
|
+
fs.unlinkSync(filePath);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
try { fs.unlinkSync(filePath + '.lock'); } catch {}
|
|
424
|
+
process.stdout.write(formatDeferredResult(filePath, result, qaMode, unusedFollowupText, header));
|
|
425
|
+
removeQueuedFollowups(filePath);
|
|
426
|
+
process.exit(0);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// āā Broadcast (ocmux "Space" multi-session send) āāāāāāāāāāāāāāāāāāā
|
|
430
|
+
|
|
431
|
+
// One answer section: a heading with the session name and a separator below it.
|
|
432
|
+
function buildBroadcastSection(title, id, answer) {
|
|
433
|
+
let s = `\nš¬ ${title} [${id}]\n${'ā'.repeat(40)}\n`;
|
|
434
|
+
s += answer;
|
|
435
|
+
if (!answer.endsWith('\n')) s += '\n';
|
|
436
|
+
return s;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Send a prompt to every session in `ids` and collect the answers.
|
|
440
|
+
// deps.getActiveSessions(server) -> Set of busy session ids
|
|
441
|
+
// deps.getSession(server, id) -> session object (title)
|
|
442
|
+
// deps.sendToSession(server, id, text) -> answer text
|
|
443
|
+
// deps.readBroadcast() -> current .ocmux.json broadcast list (null/[] = cancelled)
|
|
444
|
+
// Waits for each session to become idle before sending; stops as soon as the
|
|
445
|
+
// broadcast list disappears (ocmux exited broadcast mode early).
|
|
446
|
+
async function runBroadcastSend(server, ids, prompt, deps) {
|
|
447
|
+
const { getActiveSessions, getSession, sendToSession, readBroadcast, getPrompt, pollMs = 1000, busyTimeoutMs = 15 * 60 * 1000 } = deps;
|
|
448
|
+
const initial = new Set(ids.map(String));
|
|
449
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
450
|
+
const cancelled = () => {
|
|
451
|
+
try { return !isBroadcastActive(readBroadcast(), initial); } catch { return false; }
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Race a sent session's answer against ocmux leaving broadcast mode. We do
|
|
455
|
+
// not interrupt the model: we merely stop waiting/discard its later answer,
|
|
456
|
+
// exactly as the UI contract promises.
|
|
457
|
+
const awaitAnswerOrCancel = (promise) => new Promise((resolve) => {
|
|
458
|
+
let done = false;
|
|
459
|
+
const finish = (value) => {
|
|
460
|
+
if (done) return;
|
|
461
|
+
done = true;
|
|
462
|
+
clearInterval(timer);
|
|
463
|
+
resolve(value);
|
|
464
|
+
};
|
|
465
|
+
const timer = setInterval(() => {
|
|
466
|
+
if (cancelled()) finish({ cancelled: true });
|
|
467
|
+
}, pollMs);
|
|
468
|
+
promise.then(answer => finish({ answer }), err => finish({ error: err }));
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const runOne = async (id) => {
|
|
472
|
+
let title = id;
|
|
473
|
+
try {
|
|
474
|
+
const s = await getSession(server, id);
|
|
475
|
+
title = (s && (s.title || s.id)) || id;
|
|
476
|
+
} catch {
|
|
477
|
+
return { missing: { id, title, error: 'Unable to read session', timestamp: new Date().toISOString() } };
|
|
478
|
+
}
|
|
479
|
+
// Wait for the session to become idle (skip prompts into a busy session).
|
|
480
|
+
const deadline = Date.now() + busyTimeoutMs;
|
|
481
|
+
let busy = true;
|
|
482
|
+
while (busy && Date.now() < deadline) {
|
|
483
|
+
if (cancelled()) {
|
|
484
|
+
return { missing: { id, title, error: 'Broadcast cancelled while waiting for idle', timestamp: new Date().toISOString() } };
|
|
485
|
+
}
|
|
486
|
+
const active = await getActiveSessions(server);
|
|
487
|
+
if (!active.has(String(id))) busy = false;
|
|
488
|
+
else await sleep(pollMs);
|
|
489
|
+
}
|
|
490
|
+
if (busy) {
|
|
491
|
+
return { missing: { id, title, error: 'Session stayed busy until timeout', timestamp: new Date().toISOString() } };
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const result = await awaitAnswerOrCancel(sendToSession(server, id, getPrompt ? getPrompt(id, prompt) : prompt));
|
|
495
|
+
if (result.cancelled) {
|
|
496
|
+
return { missing: { id, title, error: 'Broadcast cancelled before this session replied', timestamp: new Date().toISOString() } };
|
|
497
|
+
}
|
|
498
|
+
if (result.error) {
|
|
499
|
+
return { missing: { id, title, error: result.error.message || String(result.error), timestamp: new Date().toISOString() } };
|
|
500
|
+
}
|
|
501
|
+
return { result: { id, title, answer: result.answer } };
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return { missing: { id, title, error: err.message || String(err), timestamp: new Date().toISOString() } };
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Run independently so one busy/model-limited session never prevents all
|
|
508
|
+
// other selected sessions from receiving the prompt.
|
|
509
|
+
const settled = await Promise.all(ids.map(runOne));
|
|
510
|
+
const results = settled.filter(x => x.result).map(x => x.result);
|
|
511
|
+
const missing = settled.filter(x => x.missing).map(x => x.missing);
|
|
512
|
+
return { results, missing, requested: ids.length };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// A broadcast list is still active while it is an array that contains every
|
|
516
|
+
// session we were asked to reach.
|
|
517
|
+
function isBroadcastActive(broadcast, initial) {
|
|
518
|
+
return Array.isArray(broadcast)
|
|
519
|
+
&& broadcast.length >= 2
|
|
520
|
+
&& [...initial].every((id) => broadcast.includes(id));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Assemble the final broadcast output: heading per session + missing note.
|
|
524
|
+
function formatBroadcastResults({ results, missing, requested }) {
|
|
525
|
+
let out = `š Broadcast to ${requested} sessions\n`;
|
|
526
|
+
for (const r of results) out += buildBroadcastSection(r.title, r.id, r.answer);
|
|
527
|
+
if (missing.length > 0) {
|
|
528
|
+
out += '\nā ļø Prompt not completed in the following sessions:\n';
|
|
529
|
+
for (const m of missing) {
|
|
530
|
+
out += `- š¬ ${m.title} [${m.id}]\n - Error: ${m.error || 'Unknown'}\n`;
|
|
531
|
+
if (m.timestamp) out += ` - At: ${m.timestamp}\n`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Structured header (project path + session id/title) prepended to --qa output,
|
|
538
|
+
// so a piped answer is self-describing about where it came from.
|
|
539
|
+
async function buildSessionHeader(server, sessionId) {
|
|
540
|
+
let title = sessionId;
|
|
541
|
+
let dir = '';
|
|
542
|
+
try {
|
|
543
|
+
const s = await getSession(server, sessionId);
|
|
544
|
+
if (s && (s.title || s.id)) title = s.title || s.id;
|
|
545
|
+
if (s && s.location && s.location.directory) dir = s.location.directory;
|
|
546
|
+
} catch {
|
|
547
|
+
// keep the plain id form
|
|
548
|
+
}
|
|
549
|
+
let out = '';
|
|
550
|
+
if (dir) out += `š ${dir}\n`;
|
|
551
|
+
out += `š¬ ${title} [${sessionId}]\n`;
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Header for deferred-ticket retrieval (ticket carries server + sessionId).
|
|
556
|
+
async function ticketHeader(ticket, qaMode) {
|
|
557
|
+
if (qaMode && ticket && ticket.server && ticket.sessionId && !(Array.isArray(ticket.sessionIds) && ticket.sessionIds.length >= 2)) {
|
|
558
|
+
try { return await buildSessionHeader(ticket.server, ticket.sessionId); } catch {}
|
|
559
|
+
}
|
|
560
|
+
return '';
|
|
320
561
|
}
|
|
321
562
|
|
|
322
563
|
async function readAllStdin() {
|
|
@@ -333,18 +574,79 @@ const HUMAN_ROW = 'š¤: āāāāāāāāāāāāāāāāā\n'
|
|
|
333
574
|
const AGENT_ROW = 'š¤: āāāāāāāāāāāāāāāāā\n'
|
|
334
575
|
const FINAL_ROW = ' āāāāāāāāāāāāāāāāā\n';
|
|
335
576
|
|
|
577
|
+
// Write the prompt to a temp file, lock the output file, and spawn a detached
|
|
578
|
+
// `--defer-child` process that runs the prompt and writes the answer. Returns
|
|
579
|
+
// the ticket describing this deferred run.
|
|
580
|
+
function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, qaMode, tgMode, flushMode, targetSessionId, targetSessionIds, deferSeconds }) {
|
|
581
|
+
const tempPath = generateTempFilename();
|
|
582
|
+
const promptTempPath = tempPath + '.prompt';
|
|
583
|
+
|
|
584
|
+
fs.writeFileSync(promptTempPath, promptText);
|
|
585
|
+
fs.writeFileSync(tempPath, '');
|
|
586
|
+
lockFile(tempPath);
|
|
587
|
+
|
|
588
|
+
const ticket = {
|
|
589
|
+
ctime: new Date().toISOString(),
|
|
590
|
+
path: tempPath,
|
|
591
|
+
defer: deferSeconds || 0,
|
|
592
|
+
server: serverBase,
|
|
593
|
+
sessionId: targetSessionId,
|
|
594
|
+
sessionIds: targetSessionIds,
|
|
595
|
+
cancelled: false,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const childArgs = [
|
|
599
|
+
__filename,
|
|
600
|
+
'--defer-child',
|
|
601
|
+
'--prompt-file', promptTempPath,
|
|
602
|
+
'--output-file', tempPath,
|
|
603
|
+
];
|
|
604
|
+
if (qaMode) childArgs.push('--qa');
|
|
605
|
+
if (tgMode === true) childArgs.push('--tg');
|
|
606
|
+
if (tgMode === false) childArgs.push('--no-tg');
|
|
607
|
+
if (flushMode) childArgs.push('--flush');
|
|
608
|
+
if (sessionName) {
|
|
609
|
+
childArgs.push('--session', sessionName);
|
|
610
|
+
if (newSession) childArgs.push('--new');
|
|
611
|
+
}
|
|
612
|
+
if (targetSessionId) childArgs.push('--resolved-session-id', targetSessionId);
|
|
613
|
+
if (Array.isArray(targetSessionIds) && targetSessionIds.length > 0) {
|
|
614
|
+
childArgs.push('--resolved-session-ids', JSON.stringify(targetSessionIds));
|
|
615
|
+
}
|
|
616
|
+
childArgs.push(serverBase);
|
|
617
|
+
|
|
618
|
+
const child = child_process.spawn(process.execPath, childArgs, {
|
|
619
|
+
detached: true,
|
|
620
|
+
stdio: 'ignore',
|
|
621
|
+
});
|
|
622
|
+
child.unref();
|
|
623
|
+
|
|
624
|
+
return ticket;
|
|
625
|
+
}
|
|
626
|
+
|
|
336
627
|
async function main() {
|
|
337
628
|
let serverBase = 'http://localhost:4096';
|
|
338
629
|
let qaMode = false;
|
|
339
630
|
let flushMode = false;
|
|
340
631
|
let deferMode = false;
|
|
341
632
|
let deferSeconds = 0;
|
|
633
|
+
// Project context from the nearest .ocmux.json (v2 model): project directory,
|
|
634
|
+
// stored session ID, and cached server URL. All null when no state file
|
|
635
|
+
// applies, in which case agentp keeps its legacy (unscoped) behavior.
|
|
636
|
+
const ctx = projectState.resolveContext();
|
|
637
|
+
const projectDir = ctx.directory;
|
|
638
|
+
const stateSession = ctx.session;
|
|
639
|
+
// Adopt the project's cached server (from .ocmux.json) unless an explicit
|
|
640
|
+
// server/port is given later in the argument loop (--getLast runs mid-loop,
|
|
641
|
+
// so adoption must happen before it).
|
|
642
|
+
if (ctx.server) serverBase = ctx.server;
|
|
342
643
|
let deferChildMode = false;
|
|
343
644
|
let onlineTicket = false;
|
|
344
645
|
let promptFile = null;
|
|
345
646
|
let outputFile = null;
|
|
346
647
|
let sessionName = null;
|
|
347
648
|
let resolvedSessionId = null;
|
|
649
|
+
let resolvedSessionIds = null;
|
|
348
650
|
let newSession = false;
|
|
349
651
|
// tgMode: false = disabled, 'auto' = auto-detect, true = explicit --tg
|
|
350
652
|
let tgMode = null;
|
|
@@ -373,6 +675,7 @@ async function main() {
|
|
|
373
675
|
console.error(' --getLast Retrieve last N assistant answers (or QA pairs with --qa) from session history');
|
|
374
676
|
console.error(' --session Target a specific session by name (creates it if not found with --new)');
|
|
375
677
|
console.error(' --new Create a new session with the given title (requires --session)');
|
|
678
|
+
console.error(' --server OpenCode server URL (overrides .ocmux.json); e.g. http://host:4096');
|
|
376
679
|
console.error(' --help Show this help message');
|
|
377
680
|
console.error('');
|
|
378
681
|
console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
|
|
@@ -391,8 +694,9 @@ async function main() {
|
|
|
391
694
|
console.error('');
|
|
392
695
|
console.error('Arguments:');
|
|
393
696
|
console.error(' url OpenCode server URL or port number (default: 4096)');
|
|
394
|
-
console.error('
|
|
395
|
-
console.error('
|
|
697
|
+
console.error(' Example: 4096, http://localhost:4096, http://192.168.1.50:4096');
|
|
698
|
+
console.error(' Omit it to resolve from the nearest .ocmux.json');
|
|
699
|
+
console.error(' (its directory selects the project, its session id the target).');
|
|
396
700
|
console.error('');
|
|
397
701
|
console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
|
|
398
702
|
console.error('');
|
|
@@ -445,6 +749,16 @@ async function main() {
|
|
|
445
749
|
console.error('Error: --resolved-session-id requires a session id');
|
|
446
750
|
process.exit(1);
|
|
447
751
|
}
|
|
752
|
+
} else if (args[i] === '--resolved-session-ids') {
|
|
753
|
+
i++;
|
|
754
|
+
try {
|
|
755
|
+
const ids = JSON.parse(args[i]);
|
|
756
|
+
if (!Array.isArray(ids) || ids.length < 2 || ids.some(id => typeof id !== 'string' || !id)) throw new Error('bad ids');
|
|
757
|
+
resolvedSessionIds = ids;
|
|
758
|
+
} catch {
|
|
759
|
+
console.error('Error: --resolved-session-ids requires a JSON array of at least two session ids');
|
|
760
|
+
process.exit(1);
|
|
761
|
+
}
|
|
448
762
|
} else if (args[i] === '--new') {
|
|
449
763
|
newSession = true;
|
|
450
764
|
} else if (args[i] === '--getLast') {
|
|
@@ -456,18 +770,12 @@ async function main() {
|
|
|
456
770
|
}
|
|
457
771
|
const n = parseInt(nStr, 10);
|
|
458
772
|
try {
|
|
459
|
-
const sessions = await listSessions(serverBase);
|
|
773
|
+
const sessions = await listSessions(serverBase, projectDir || undefined);
|
|
460
774
|
if (!sessions || sessions.length === 0) {
|
|
461
775
|
console.error('No sessions found.');
|
|
462
776
|
process.exit(1);
|
|
463
777
|
}
|
|
464
|
-
const sorted = sessions
|
|
465
|
-
.filter(s => s.time && (s.time.updated || s.time.created))
|
|
466
|
-
.sort((a, b) => {
|
|
467
|
-
const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
|
|
468
|
-
const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
|
|
469
|
-
return bTime - aTime;
|
|
470
|
-
});
|
|
778
|
+
const sorted = sortSessionsByRecency(sessions);
|
|
471
779
|
const sessionId = sorted[0].id;
|
|
472
780
|
|
|
473
781
|
function buildPairsFromMessages(messages) {
|
|
@@ -548,6 +856,13 @@ async function main() {
|
|
|
548
856
|
tgMode = true;
|
|
549
857
|
} else if (args[i] === '--no-tg') {
|
|
550
858
|
tgMode = false;
|
|
859
|
+
} else if (args[i] === '--server') {
|
|
860
|
+
i++;
|
|
861
|
+
if (!args[i]) {
|
|
862
|
+
console.error("Error: '--server' requires a URL (e.g. http://host:4096)");
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
serverBase = args[i].replace(/\/+$/, '');
|
|
551
866
|
} else if (args[i].includes('://')) {
|
|
552
867
|
serverBase = args[i].replace(/\/+$/, '');
|
|
553
868
|
} else if (/^\d+$/.test(args[i])) {
|
|
@@ -563,6 +878,8 @@ async function main() {
|
|
|
563
878
|
}
|
|
564
879
|
}
|
|
565
880
|
|
|
881
|
+
const preferredSession = (sessionName || newSession || resolvedSessionId) ? null : stateSession;
|
|
882
|
+
|
|
566
883
|
// Resolve default: --qa ā auto-detect, standalone ā disabled
|
|
567
884
|
if (tgMode === null) {
|
|
568
885
|
tgMode = qaMode ? 'auto' : false;
|
|
@@ -596,6 +913,10 @@ async function main() {
|
|
|
596
913
|
// ignored; the ticket's own "defer" timeout is used instead (default 0).
|
|
597
914
|
const deferredPath = ticket.path;
|
|
598
915
|
|
|
916
|
+
if (ticket.cancelled) {
|
|
917
|
+
await cancelTicket(ticket);
|
|
918
|
+
}
|
|
919
|
+
|
|
599
920
|
if (!fs.existsSync(deferredPath)) {
|
|
600
921
|
process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
|
|
601
922
|
process.exit(1);
|
|
@@ -614,7 +935,7 @@ async function main() {
|
|
|
614
935
|
process.exit(1);
|
|
615
936
|
}
|
|
616
937
|
try { fs.unlinkSync(deferredPath + '.lock'); } catch {}
|
|
617
|
-
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ticket.followupText));
|
|
938
|
+
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ticket.followupText, await ticketHeader(ticket, qaMode)));
|
|
618
939
|
removeQueuedFollowups(deferredPath);
|
|
619
940
|
process.exit(0);
|
|
620
941
|
}
|
|
@@ -643,7 +964,7 @@ async function main() {
|
|
|
643
964
|
process.exit(1);
|
|
644
965
|
}
|
|
645
966
|
try { fs.unlinkSync(deferredPath + '.lock'); } catch {}
|
|
646
|
-
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ''));
|
|
967
|
+
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, '', await ticketHeader(ticket, qaMode)));
|
|
647
968
|
removeQueuedFollowups(deferredPath);
|
|
648
969
|
process.exit(0);
|
|
649
970
|
}
|
|
@@ -658,70 +979,49 @@ async function main() {
|
|
|
658
979
|
}
|
|
659
980
|
|
|
660
981
|
// This is a regular prompt ā create deferred execution
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
lockFile(tempPath);
|
|
678
|
-
|
|
679
|
-
const newTicket = {
|
|
680
|
-
ctime: new Date().toISOString(),
|
|
681
|
-
path: tempPath,
|
|
682
|
-
defer: deferSeconds,
|
|
683
|
-
server: serverBase,
|
|
684
|
-
sessionId: targetSessionId,
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// Spawn detached child process
|
|
688
|
-
const childArgs = [
|
|
689
|
-
__filename,
|
|
690
|
-
'--defer-child',
|
|
691
|
-
'--prompt-file', promptTempPath,
|
|
692
|
-
'--output-file', tempPath,
|
|
693
|
-
];
|
|
694
|
-
|
|
695
|
-
// Pass through other flags
|
|
696
|
-
if (qaMode) childArgs.push('--qa');
|
|
697
|
-
if (tgMode === true) childArgs.push('--tg');
|
|
698
|
-
if (tgMode === false) childArgs.push('--no-tg');
|
|
699
|
-
if (flushMode) childArgs.push('--flush');
|
|
700
|
-
if (sessionName) {
|
|
701
|
-
childArgs.push('--session', sessionName);
|
|
702
|
-
if (newSession) childArgs.push('--new');
|
|
982
|
+
const broadcastIds = (resolvedSessionIds && resolvedSessionIds.length >= 2)
|
|
983
|
+
? resolvedSessionIds
|
|
984
|
+
: ((ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
985
|
+
? ctx.state.broadcast
|
|
986
|
+
: null);
|
|
987
|
+
let targetSessionId = null;
|
|
988
|
+
if (!broadcastIds) {
|
|
989
|
+
try {
|
|
990
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
991
|
+
directory: projectDir,
|
|
992
|
+
preferredSession,
|
|
993
|
+
});
|
|
994
|
+
} catch (err) {
|
|
995
|
+
console.error(err.message);
|
|
996
|
+
process.exit(1);
|
|
997
|
+
}
|
|
703
998
|
}
|
|
704
|
-
childArgs.push('--resolved-session-id', targetSessionId);
|
|
705
|
-
childArgs.push(serverBase);
|
|
706
999
|
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
1000
|
+
const newTicket = spawnDeferredChild({
|
|
1001
|
+
promptText,
|
|
1002
|
+
serverBase,
|
|
1003
|
+
sessionName,
|
|
1004
|
+
newSession,
|
|
1005
|
+
qaMode,
|
|
1006
|
+
tgMode,
|
|
1007
|
+
flushMode,
|
|
1008
|
+
targetSessionId,
|
|
1009
|
+
targetSessionIds: broadcastIds,
|
|
1010
|
+
deferSeconds,
|
|
710
1011
|
});
|
|
711
|
-
child.unref();
|
|
712
1012
|
|
|
713
1013
|
// If a timeout was given, block until the answer arrives or the timeout
|
|
714
1014
|
// expires. Return the response when it arrives; otherwise print a ticket.
|
|
715
1015
|
if (deferSeconds > 0) {
|
|
716
|
-
const result = await waitForDeferredResult(
|
|
1016
|
+
const result = await waitForDeferredResult(newTicket.path, deferSeconds * 1000);
|
|
717
1017
|
if (result !== null) {
|
|
718
1018
|
try {
|
|
719
|
-
fs.unlinkSync(
|
|
1019
|
+
fs.unlinkSync(newTicket.path);
|
|
720
1020
|
} catch (err) {
|
|
721
1021
|
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
722
1022
|
process.exit(1);
|
|
723
1023
|
}
|
|
724
|
-
try { fs.unlinkSync(
|
|
1024
|
+
try { fs.unlinkSync(newTicket.path + '.lock'); } catch {}
|
|
725
1025
|
process.stdout.write(result);
|
|
726
1026
|
process.exit(0);
|
|
727
1027
|
}
|
|
@@ -732,6 +1032,99 @@ async function main() {
|
|
|
732
1032
|
process.exit(0);
|
|
733
1033
|
}
|
|
734
1034
|
|
|
1035
|
+
// Normal (non-deferred, non-child) mode: wait for the answer, but run it in a
|
|
1036
|
+
// detached child so a Ctrl+C can hand the in-flight response off as a ticket.
|
|
1037
|
+
if (!deferChildMode) {
|
|
1038
|
+
const ticket = parseDeferredTicketInput(promptText);
|
|
1039
|
+
|
|
1040
|
+
if (ticket) {
|
|
1041
|
+
// Recognize a deferred ticket: wait indefinitely for the answer, and
|
|
1042
|
+
// re-print the ticket only if the user interrupts with Ctrl+C.
|
|
1043
|
+
const deferredPath = ticket.path;
|
|
1044
|
+
|
|
1045
|
+
if (ticket.cancelled) {
|
|
1046
|
+
await cancelTicket(ticket);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (!fs.existsSync(deferredPath)) {
|
|
1050
|
+
process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
|
|
1051
|
+
process.exit(1);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
let unusedFollowup = ticket.followupText || '';
|
|
1055
|
+
|
|
1056
|
+
// If the answer is already ready, return it immediately.
|
|
1057
|
+
let result = ticket.followupText ? readDeferredResult(deferredPath) : null;
|
|
1058
|
+
if (result !== null) {
|
|
1059
|
+
outputReadyResult(deferredPath, result, qaMode, unusedFollowup, await ticketHeader(ticket, qaMode));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Queue any follow-up text to the original session (same as --defer).
|
|
1063
|
+
if (ticket.followupText) {
|
|
1064
|
+
if (!ticket.server || !ticket.sessionId) {
|
|
1065
|
+
process.stdout.write('Error: deferred ticket does not include server/sessionId; cannot queue follow-up text. Re-submit only the ticket to check the result.\n');
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
await sendToSessionAsync(ticket.server, ticket.sessionId, ticket.followupText);
|
|
1070
|
+
appendQueuedFollowup(deferredPath, ticket.followupText);
|
|
1071
|
+
unusedFollowup = ''; // follow-up already queued; don't echo it again
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
process.stdout.write(`Error queueing follow-up prompt: ${err.message}\n`);
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const wait = await waitForDeferredResultForever(deferredPath);
|
|
1079
|
+
if (wait.interrupted) printTicketAndExit(ticket, onlineTicket);
|
|
1080
|
+
outputReadyResult(deferredPath, wait.result, qaMode, unusedFollowup, await ticketHeader(ticket, qaMode));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Regular prompt: submit via a detached child and wait for the answer.
|
|
1084
|
+
const broadcastIds = (ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
1085
|
+
? ctx.state.broadcast
|
|
1086
|
+
: null;
|
|
1087
|
+
let targetSessionId = null;
|
|
1088
|
+
if (!broadcastIds) {
|
|
1089
|
+
try {
|
|
1090
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
1091
|
+
directory: projectDir,
|
|
1092
|
+
preferredSession,
|
|
1093
|
+
});
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
console.error(err.message);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const newTicket = spawnDeferredChild({
|
|
1101
|
+
promptText,
|
|
1102
|
+
serverBase,
|
|
1103
|
+
sessionName,
|
|
1104
|
+
newSession,
|
|
1105
|
+
qaMode,
|
|
1106
|
+
tgMode,
|
|
1107
|
+
flushMode,
|
|
1108
|
+
targetSessionId,
|
|
1109
|
+
targetSessionIds: broadcastIds,
|
|
1110
|
+
deferSeconds: 0,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const wait = await waitForDeferredResultForever(newTicket.path);
|
|
1114
|
+
if (wait.interrupted) printTicketAndExit(newTicket, onlineTicket);
|
|
1115
|
+
|
|
1116
|
+
// Answer ready ā output it and clean up.
|
|
1117
|
+
try {
|
|
1118
|
+
fs.unlinkSync(newTicket.path);
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
try { fs.unlinkSync(newTicket.path + '.lock'); } catch {}
|
|
1124
|
+
process.stdout.write(wait.result);
|
|
1125
|
+
process.exit(0);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
735
1128
|
// Handle --defer-child mode (child process)
|
|
736
1129
|
let actualPromptText = promptText;
|
|
737
1130
|
if (deferChildMode && promptFile) {
|
|
@@ -749,21 +1142,69 @@ async function main() {
|
|
|
749
1142
|
// Wrap main processing in try-catch for deferChildMode to write errors to output file
|
|
750
1143
|
let tgError = null;
|
|
751
1144
|
async function runMain() {
|
|
752
|
-
|
|
1145
|
+
// Broadcast mode: ocmux's Space key selected multiple sessions in
|
|
1146
|
+
// .ocmux.json ā send the prompt to all of them.
|
|
1147
|
+
const broadcastIds = (ctx && ctx.state && Array.isArray(ctx.state.broadcast) && ctx.state.broadcast.length >= 2)
|
|
1148
|
+
? ctx.state.broadcast
|
|
1149
|
+
: null;
|
|
753
1150
|
|
|
1151
|
+
let sessionId = null;
|
|
754
1152
|
let answer;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1153
|
+
if (broadcastIds) {
|
|
1154
|
+
try { if (projectDir) ocmuxLib.focusWindowByDir(projectDir); } catch {}
|
|
1155
|
+
const bres = await runBroadcastSend(serverBase, broadcastIds, actualPromptText, {
|
|
1156
|
+
getActiveSessions,
|
|
1157
|
+
getSession,
|
|
1158
|
+
sendToSession,
|
|
1159
|
+
getPrompt: (id, text) => {
|
|
1160
|
+
try {
|
|
1161
|
+
const ann = ctx && ctx.statefile && projectState.readAnnotations(ctx.statefile)[id];
|
|
1162
|
+
return ann ? ann + '\n\n' + text : text;
|
|
1163
|
+
} catch {
|
|
1164
|
+
return text;
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
readBroadcast: () => (ctx && ctx.statefile ? projectState.readProjectState(ctx.statefile).broadcast : null),
|
|
1168
|
+
});
|
|
1169
|
+
answer = formatBroadcastResults(bres);
|
|
1170
|
+
} else {
|
|
1171
|
+
sessionId = resolvedSessionId || await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
1172
|
+
directory: projectDir,
|
|
1173
|
+
preferredSession,
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// v2: focus the target project's TUI window so the user sees the prompt
|
|
1177
|
+
// stream, and prepend the session's annotation (if any) to the prompt.
|
|
1178
|
+
let prompt = actualPromptText;
|
|
1179
|
+
try {
|
|
1180
|
+
if (ctx && ctx.statefile) {
|
|
1181
|
+
const ann = projectState.readAnnotations(ctx.statefile)[sessionId];
|
|
1182
|
+
if (ann) prompt = ann + '\n\n' + actualPromptText;
|
|
1183
|
+
}
|
|
1184
|
+
if (projectDir) ocmuxLib.focusWindowByDir(projectDir);
|
|
1185
|
+
} catch {
|
|
1186
|
+
// tmux focus is best-effort; never fail the prompt because of it
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
answer = await sendToSession(serverBase, sessionId, prompt);
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
throw new Error(`Error sending prompt: ${err.message}`);
|
|
1193
|
+
}
|
|
759
1194
|
}
|
|
760
1195
|
|
|
761
1196
|
// Build the message to send to Telegram (QA pair with rulers)
|
|
762
1197
|
let tgMessage = answer;
|
|
763
|
-
if (qaMode) {
|
|
1198
|
+
if (qaMode && broadcastIds) {
|
|
764
1199
|
tgMessage = HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
|
|
765
1200
|
if (!answer.endsWith('\n')) tgMessage += '\n';
|
|
766
1201
|
tgMessage += FINAL_ROW;
|
|
1202
|
+
} else if (qaMode && !broadcastIds) {
|
|
1203
|
+
let header = '';
|
|
1204
|
+
try { header = await buildSessionHeader(serverBase, sessionId); } catch {}
|
|
1205
|
+
tgMessage = header + HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
|
|
1206
|
+
if (!answer.endsWith('\n')) tgMessage += '\n';
|
|
1207
|
+
tgMessage += FINAL_ROW;
|
|
767
1208
|
}
|
|
768
1209
|
|
|
769
1210
|
// Post-send: notify tgagentp, get recorded context back
|
|
@@ -843,4 +1284,4 @@ if (require.main === module) {
|
|
|
843
1284
|
});
|
|
844
1285
|
}
|
|
845
1286
|
|
|
846
|
-
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult };
|
|
1287
|
+
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult, waitForDeferredResultForever, runBroadcastSend, buildBroadcastSection, formatBroadcastResults, isBroadcastActive };
|