@coinseeker/opencode-telegram-plugin 1.0.8 → 1.0.10
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 +2 -2
- package/dist/telegram-remote.js +55 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,11 @@ Configure the npm package in `~/.config/opencode/opencode.json`:
|
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"plugin": ["@coinseeker/opencode-telegram-plugin@1.0.
|
|
18
|
+
"plugin": ["@coinseeker/opencode-telegram-plugin@1.0.10"]
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.
|
|
22
|
+
Current stable version: `@coinseeker/opencode-telegram-plugin@1.0.10`.
|
|
23
23
|
|
|
24
24
|
Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
|
|
25
25
|
|
package/dist/telegram-remote.js
CHANGED
|
@@ -45,7 +45,6 @@ function createTelegramBot(opts) {
|
|
|
45
45
|
let activeChatId = opts.initialChatId;
|
|
46
46
|
let questionDispatcher;
|
|
47
47
|
let permissionDispatcher;
|
|
48
|
-
let startWorkDispatcher;
|
|
49
48
|
if (polling) {
|
|
50
49
|
bot.use(async (ctx, next) => {
|
|
51
50
|
const userId = ctx.from?.id;
|
|
@@ -98,13 +97,6 @@ This chat is now active for OpenCode notifications.`
|
|
|
98
97
|
if (!permissionDispatcher || messageId === void 0) return;
|
|
99
98
|
await permissionDispatcher.handleCallbackQuery(data, messageId);
|
|
100
99
|
});
|
|
101
|
-
bot.callbackQuery(/^sw:(.+)$/, async (ctx) => {
|
|
102
|
-
await ctx.answerCallbackQuery();
|
|
103
|
-
const data = ctx.callbackQuery.data;
|
|
104
|
-
const messageId = ctx.callbackQuery.message?.message_id;
|
|
105
|
-
if (!startWorkDispatcher || messageId === void 0) return;
|
|
106
|
-
await startWorkDispatcher.handleCallbackQuery(data, messageId);
|
|
107
|
-
});
|
|
108
100
|
bot.on("message:text", async (ctx) => {
|
|
109
101
|
const replyToMessageId = ctx.message.reply_to_message?.message_id;
|
|
110
102
|
const chatId = ctx.chat.id;
|
|
@@ -198,9 +190,6 @@ This chat is now active for OpenCode notifications.`
|
|
|
198
190
|
},
|
|
199
191
|
setPermissionDispatcher(dispatcher) {
|
|
200
192
|
permissionDispatcher = dispatcher;
|
|
201
|
-
},
|
|
202
|
-
setStartWorkDispatcher(dispatcher) {
|
|
203
|
-
startWorkDispatcher = dispatcher;
|
|
204
193
|
}
|
|
205
194
|
};
|
|
206
195
|
}
|
|
@@ -482,6 +471,7 @@ async function handleNormalizedPermission(permission, ctx) {
|
|
|
482
471
|
const pending = {
|
|
483
472
|
requestID: permission.requestID,
|
|
484
473
|
sessionID: permission.sessionID,
|
|
474
|
+
serverUrl: ctx.serverUrl.href,
|
|
485
475
|
title: permission.title,
|
|
486
476
|
permission: permission.permission,
|
|
487
477
|
patterns: permission.patterns,
|
|
@@ -525,7 +515,13 @@ function createPermissionDispatcher(ctx) {
|
|
|
525
515
|
return;
|
|
526
516
|
}
|
|
527
517
|
try {
|
|
528
|
-
await ctx.replyToPermission(
|
|
518
|
+
await ctx.replyToPermission(
|
|
519
|
+
pending.requestID,
|
|
520
|
+
pending.sessionID,
|
|
521
|
+
reply,
|
|
522
|
+
pending.endpoint,
|
|
523
|
+
pending.serverUrl
|
|
524
|
+
);
|
|
529
525
|
await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
|
|
530
526
|
|
|
531
527
|
${pending.permission}: ${pending.title}`);
|
|
@@ -730,7 +726,7 @@ async function completeIfReady(ctx, pending, shortHash) {
|
|
|
730
726
|
const answers = pending.answersInProgress.map((answer) => answer ?? []);
|
|
731
727
|
const messageId = pending.telegramMessageIds[0];
|
|
732
728
|
try {
|
|
733
|
-
await ctx.replyToQuestion(pending.requestID, answers);
|
|
729
|
+
await ctx.replyToQuestion(pending.requestID, answers, pending.serverUrl);
|
|
734
730
|
await ctx.bot.editMessageRemoveKeyboard(
|
|
735
731
|
messageId,
|
|
736
732
|
`\u2705 Answered:
|
|
@@ -770,6 +766,7 @@ async function handleQuestionAsked(event, ctx) {
|
|
|
770
766
|
const pending = {
|
|
771
767
|
requestID: request.id,
|
|
772
768
|
sessionID: request.sessionID,
|
|
769
|
+
serverUrl: ctx.serverUrl.href,
|
|
773
770
|
questions: request.questions,
|
|
774
771
|
sentAt,
|
|
775
772
|
expiresAt: sentAt + QUESTION_EXPIRY_MS,
|
|
@@ -953,84 +950,6 @@ async function handleSessionError(event, ctx) {
|
|
|
953
950
|
ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
|
|
954
951
|
}
|
|
955
952
|
|
|
956
|
-
// src/events/start-work.ts
|
|
957
|
-
var CALLBACK_RE3 = /^sw:(.+)$/;
|
|
958
|
-
var START_WORK_COMMAND = "start-work";
|
|
959
|
-
var START_WORK_RE = /(?:^|[\s`])\/start-work(?:\s+([^\n`]+))?/g;
|
|
960
|
-
var StartWorkCommandStore = class {
|
|
961
|
-
commands = /* @__PURE__ */ new Map();
|
|
962
|
-
updateFromText(sessionID, text) {
|
|
963
|
-
const command = extractStartWorkCommand(sessionID, text);
|
|
964
|
-
if (!command) return void 0;
|
|
965
|
-
this.commands.set(sessionID, command.arguments);
|
|
966
|
-
return command;
|
|
967
|
-
}
|
|
968
|
-
get(sessionID) {
|
|
969
|
-
const args = this.commands.get(sessionID);
|
|
970
|
-
if (args === void 0) return void 0;
|
|
971
|
-
return { sessionID, arguments: args };
|
|
972
|
-
}
|
|
973
|
-
delete(sessionID) {
|
|
974
|
-
this.commands.delete(sessionID);
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
function extractStartWorkCommand(sessionID, text) {
|
|
978
|
-
let latestArgs;
|
|
979
|
-
for (const match of text.matchAll(START_WORK_RE)) {
|
|
980
|
-
const args = (match[1] ?? "").trim();
|
|
981
|
-
if (args) latestArgs = args;
|
|
982
|
-
}
|
|
983
|
-
if (latestArgs === void 0) return void 0;
|
|
984
|
-
return { sessionID, arguments: latestArgs };
|
|
985
|
-
}
|
|
986
|
-
function startWorkCallbackData(sessionID) {
|
|
987
|
-
const data = `sw:${encodeURIComponent(sessionID)}`;
|
|
988
|
-
return Buffer.byteLength(data, "utf8") <= 64 ? data : void 0;
|
|
989
|
-
}
|
|
990
|
-
function startWorkKeyboard(sessionID) {
|
|
991
|
-
const callbackData = startWorkCallbackData(sessionID);
|
|
992
|
-
if (!callbackData) return void 0;
|
|
993
|
-
return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
|
|
994
|
-
}
|
|
995
|
-
function createStartWorkDispatcher(ctx) {
|
|
996
|
-
return {
|
|
997
|
-
async handleCallbackQuery(data, messageId) {
|
|
998
|
-
const match = CALLBACK_RE3.exec(data);
|
|
999
|
-
if (!match) return;
|
|
1000
|
-
const sessionID = decodeURIComponent(match[1]);
|
|
1001
|
-
const command = ctx.startWorkCommands.get(sessionID);
|
|
1002
|
-
if (!command) {
|
|
1003
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1004
|
-
messageId,
|
|
1005
|
-
`\u26A0\uFE0F No /start-work command was detected for this session.
|
|
1006
|
-
|
|
1007
|
-
Session: ${sessionID}`
|
|
1008
|
-
);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
try {
|
|
1012
|
-
await ctx.runSessionCommand(sessionID, START_WORK_COMMAND, command.arguments);
|
|
1013
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1014
|
-
messageId,
|
|
1015
|
-
`\u25B6\uFE0F Sent /start-work ${command.arguments} to opencode.
|
|
1016
|
-
|
|
1017
|
-
Session: ${sessionID}`
|
|
1018
|
-
);
|
|
1019
|
-
ctx.startWorkCommands.delete(sessionID);
|
|
1020
|
-
ctx.logger.info("start-work command sent", { sessionID });
|
|
1021
|
-
} catch (err) {
|
|
1022
|
-
await ctx.bot.editMessageRemoveKeyboard(
|
|
1023
|
-
messageId,
|
|
1024
|
-
`\u26A0\uFE0F Failed to send /start-work to opencode.
|
|
1025
|
-
|
|
1026
|
-
Session: ${sessionID}`
|
|
1027
|
-
);
|
|
1028
|
-
ctx.logger.error("failed to send start-work command", { sessionID, error: String(err) });
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
953
|
// src/events/session-idle.ts
|
|
1035
954
|
var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
|
|
1036
955
|
function sleep(ms) {
|
|
@@ -1077,17 +996,9 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
1077
996
|
});
|
|
1078
997
|
if (!claimed) return;
|
|
1079
998
|
const title = ctx.sessionTitleService.getSessionTitle(sessionId);
|
|
1080
|
-
const
|
|
1081
|
-
const message = title ? `Agent has finished: ${title}` : "Agent has finished.";
|
|
1082
|
-
const keyboard = startWorkCommand ? startWorkKeyboard(sessionId) : void 0;
|
|
1083
|
-
const text = startWorkCommand ? `${message}
|
|
1084
|
-
|
|
1085
|
-
Plan is ready. Tap below to run /start-work ${startWorkCommand.arguments}.` : message;
|
|
999
|
+
const text = title ? `Agent has finished: ${title}` : "Agent has finished.";
|
|
1086
1000
|
try {
|
|
1087
|
-
await ctx.bot.sendMessage(
|
|
1088
|
-
text,
|
|
1089
|
-
keyboard ? { reply_markup: { inline_keyboard: keyboard } } : void 0
|
|
1090
|
-
);
|
|
1001
|
+
await ctx.bot.sendMessage(text);
|
|
1091
1002
|
ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
|
|
1092
1003
|
ctx.logger.info("idle notification sent", { sessionId, title });
|
|
1093
1004
|
} catch (err) {
|
|
@@ -1097,15 +1008,19 @@ Plan is ready. Tap below to run /start-work ${startWorkCommand.arguments}.` : me
|
|
|
1097
1008
|
async function flushDeferredParentIfReady(parentID, ctx) {
|
|
1098
1009
|
if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
|
|
1099
1010
|
if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) return;
|
|
1100
|
-
|
|
1011
|
+
const parentStatus = ctx.sessionTitleService.getSessionStatus(parentID);
|
|
1012
|
+
if (parentStatus === "idle") {
|
|
1013
|
+
ctx.logger.info("keeping deferred parent idle notification - waiting for parent to resume", {
|
|
1014
|
+
sessionId: parentID
|
|
1015
|
+
});
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (parentStatus !== void 0) {
|
|
1101
1019
|
ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
|
|
1102
1020
|
ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
|
|
1103
1021
|
sessionId: parentID
|
|
1104
1022
|
});
|
|
1105
|
-
return;
|
|
1106
1023
|
}
|
|
1107
|
-
ctx.logger.info("sending deferred parent idle notification", { sessionId: parentID });
|
|
1108
|
-
await sendIdleNotification(parentID, ctx);
|
|
1109
1024
|
}
|
|
1110
1025
|
async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
|
|
1111
1026
|
await hydrateDescendants(sessionId, ctx);
|
|
@@ -1118,8 +1033,8 @@ async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
|
|
|
1118
1033
|
}
|
|
1119
1034
|
async function handleSessionIdle(event, ctx) {
|
|
1120
1035
|
const sessionId = event.properties.sessionID;
|
|
1121
|
-
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1122
1036
|
const parentID = await resolveParentID(sessionId, ctx);
|
|
1037
|
+
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1123
1038
|
if (typeof parentID === "string") {
|
|
1124
1039
|
ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
|
|
1125
1040
|
await flushDeferredParentIfReady(parentID, ctx);
|
|
@@ -1412,7 +1327,7 @@ var SessionTitleService = class {
|
|
|
1412
1327
|
const existing = this.sessions.get(sessionId);
|
|
1413
1328
|
this.sessions.set(sessionId, {
|
|
1414
1329
|
title,
|
|
1415
|
-
parentID: existing?.parentID
|
|
1330
|
+
parentID: existing?.parentID,
|
|
1416
1331
|
status: existing?.status,
|
|
1417
1332
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1418
1333
|
});
|
|
@@ -1421,7 +1336,7 @@ var SessionTitleService = class {
|
|
|
1421
1336
|
const existing = this.sessions.get(sessionId);
|
|
1422
1337
|
this.sessions.set(sessionId, {
|
|
1423
1338
|
title: existing?.title ?? null,
|
|
1424
|
-
parentID: existing?.parentID
|
|
1339
|
+
parentID: existing?.parentID,
|
|
1425
1340
|
status,
|
|
1426
1341
|
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
|
|
1427
1342
|
});
|
|
@@ -1447,7 +1362,7 @@ var SessionTitleService = class {
|
|
|
1447
1362
|
const existing = this.sessions.get(sessionId);
|
|
1448
1363
|
this.sessions.set(sessionId, {
|
|
1449
1364
|
title: existing?.title ?? null,
|
|
1450
|
-
parentID: existing?.parentID
|
|
1365
|
+
parentID: existing?.parentID,
|
|
1451
1366
|
status: existing?.status ?? "idle",
|
|
1452
1367
|
idleNotificationPending: true
|
|
1453
1368
|
});
|
|
@@ -1467,15 +1382,16 @@ var SessionTitleService = class {
|
|
|
1467
1382
|
|
|
1468
1383
|
// src/telegram-remote.ts
|
|
1469
1384
|
var pluginDir = dirname4(fileURLToPath(import.meta.url));
|
|
1470
|
-
function
|
|
1471
|
-
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1385
|
+
async function postToServer(serverUrl, path, body) {
|
|
1386
|
+
const url = new URL(path, serverUrl);
|
|
1387
|
+
const response = await fetch(url, {
|
|
1388
|
+
method: "POST",
|
|
1389
|
+
headers: { "Content-Type": "application/json" },
|
|
1390
|
+
body: JSON.stringify(body)
|
|
1391
|
+
});
|
|
1392
|
+
if (response.ok) return;
|
|
1393
|
+
const text = await response.text();
|
|
1394
|
+
throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`);
|
|
1479
1395
|
}
|
|
1480
1396
|
var TelegramRemote = async (input) => {
|
|
1481
1397
|
const logger = createLogger({ namespace: "telegram" });
|
|
@@ -1490,7 +1406,6 @@ var TelegramRemote = async (input) => {
|
|
|
1490
1406
|
const claimsDir = join6(tmpdir4(), `opencoder-telegram-claims-${tokenHash}`);
|
|
1491
1407
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1492
1408
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1493
|
-
const startWorkCommands = new StartWorkCommandStore();
|
|
1494
1409
|
const lockResult = await acquireLock({ lockPath });
|
|
1495
1410
|
const isLeader = lockResult.acquired;
|
|
1496
1411
|
logger.info(
|
|
@@ -1504,38 +1419,46 @@ var TelegramRemote = async (input) => {
|
|
|
1504
1419
|
});
|
|
1505
1420
|
const sessionTitleService = new SessionTitleService();
|
|
1506
1421
|
const client = input.client;
|
|
1507
|
-
const replyToQuestion = async (requestID, answers) => {
|
|
1422
|
+
const replyToQuestion = async (requestID, answers, serverUrl = input.serverUrl.href) => {
|
|
1423
|
+
const path = `/question/${encodeURIComponent(requestID)}/reply`;
|
|
1424
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1425
|
+
await postToServer(serverUrl, path, { answers });
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1508
1428
|
await client._client.post({
|
|
1509
|
-
url:
|
|
1429
|
+
url: path,
|
|
1510
1430
|
headers: { "Content-Type": "application/json" },
|
|
1511
1431
|
body: { answers },
|
|
1512
1432
|
throwOnError: true
|
|
1513
1433
|
});
|
|
1514
1434
|
};
|
|
1515
|
-
const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
|
|
1435
|
+
const replyToPermission = async (requestID, sessionID, reply, endpoint, serverUrl = input.serverUrl.href) => {
|
|
1516
1436
|
if (endpoint === "request") {
|
|
1437
|
+
const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
|
|
1438
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1439
|
+
await postToServer(serverUrl, path2, { reply });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1517
1442
|
await client._client.post({
|
|
1518
|
-
url:
|
|
1443
|
+
url: path2,
|
|
1519
1444
|
headers: { "Content-Type": "application/json" },
|
|
1520
1445
|
body: { reply },
|
|
1521
1446
|
throwOnError: true
|
|
1522
1447
|
});
|
|
1523
1448
|
return;
|
|
1524
1449
|
}
|
|
1450
|
+
const path = `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`;
|
|
1451
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1452
|
+
await postToServer(serverUrl, path, { response: reply });
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1525
1455
|
await client._client.post({
|
|
1526
|
-
url:
|
|
1456
|
+
url: path,
|
|
1527
1457
|
headers: { "Content-Type": "application/json" },
|
|
1528
1458
|
body: { response: reply },
|
|
1529
1459
|
throwOnError: true
|
|
1530
1460
|
});
|
|
1531
1461
|
};
|
|
1532
|
-
const runSessionCommand = async (sessionID, command, args) => {
|
|
1533
|
-
await input.client.session.command({
|
|
1534
|
-
path: { id: sessionID },
|
|
1535
|
-
body: { command, arguments: args },
|
|
1536
|
-
throwOnError: true
|
|
1537
|
-
});
|
|
1538
|
-
};
|
|
1539
1462
|
const bot = createTelegramBot({
|
|
1540
1463
|
config,
|
|
1541
1464
|
stateStore,
|
|
@@ -1580,15 +1503,12 @@ var TelegramRemote = async (input) => {
|
|
|
1580
1503
|
tokenHash,
|
|
1581
1504
|
pendingQuestions,
|
|
1582
1505
|
pendingPermissions,
|
|
1583
|
-
startWorkCommands,
|
|
1584
1506
|
replyToQuestion,
|
|
1585
|
-
replyToPermission
|
|
1586
|
-
runSessionCommand
|
|
1507
|
+
replyToPermission
|
|
1587
1508
|
};
|
|
1588
1509
|
if (isLeader) {
|
|
1589
1510
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1590
1511
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1591
|
-
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
1592
1512
|
}
|
|
1593
1513
|
return {
|
|
1594
1514
|
event: async ({ event }) => {
|
|
@@ -1606,17 +1526,6 @@ var TelegramRemote = async (input) => {
|
|
|
1606
1526
|
case "permission.updated":
|
|
1607
1527
|
return handlePermissionUpdated(event, ctx);
|
|
1608
1528
|
default: {
|
|
1609
|
-
const textPart = getTextPartFromMessagePartUpdated(extEvent);
|
|
1610
|
-
if (textPart) {
|
|
1611
|
-
const command = startWorkCommands.updateFromText(textPart.sessionID, textPart.text);
|
|
1612
|
-
if (command) {
|
|
1613
|
-
logger.info("start-work command detected", {
|
|
1614
|
-
sessionID: command.sessionID,
|
|
1615
|
-
arguments: command.arguments
|
|
1616
|
-
});
|
|
1617
|
-
}
|
|
1618
|
-
return;
|
|
1619
|
-
}
|
|
1620
1529
|
if (isEventPermissionAsked(extEvent)) {
|
|
1621
1530
|
if (!isLeader) return;
|
|
1622
1531
|
return handlePermissionAsked(extEvent, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coinseeker/opencode-telegram-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Control and monitor OpenCode from Telegram with notifications, question replies, and subagent-aware completion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/telegram-remote.js",
|