@coinseeker/opencode-telegram-plugin 1.0.9 → 1.0.11
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 +333 -51
- 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.11"]
|
|
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.11`.
|
|
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
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// src/telegram-remote.ts
|
|
7
|
-
import { createHash as
|
|
8
|
-
import { tmpdir as
|
|
9
|
-
import { dirname as
|
|
7
|
+
import { createHash as createHash5 } from "crypto";
|
|
8
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
9
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
|
|
12
12
|
// src/bot.ts
|
|
@@ -45,6 +45,7 @@ function createTelegramBot(opts) {
|
|
|
45
45
|
let activeChatId = opts.initialChatId;
|
|
46
46
|
let questionDispatcher;
|
|
47
47
|
let permissionDispatcher;
|
|
48
|
+
let startWorkDispatcher;
|
|
48
49
|
if (polling) {
|
|
49
50
|
bot.use(async (ctx, next) => {
|
|
50
51
|
const userId = ctx.from?.id;
|
|
@@ -97,6 +98,13 @@ This chat is now active for OpenCode notifications.`
|
|
|
97
98
|
if (!permissionDispatcher || messageId === void 0) return;
|
|
98
99
|
await permissionDispatcher.handleCallbackQuery(data, messageId);
|
|
99
100
|
});
|
|
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
|
+
});
|
|
100
108
|
bot.on("message:text", async (ctx) => {
|
|
101
109
|
const replyToMessageId = ctx.message.reply_to_message?.message_id;
|
|
102
110
|
const chatId = ctx.chat.id;
|
|
@@ -190,6 +198,9 @@ This chat is now active for OpenCode notifications.`
|
|
|
190
198
|
},
|
|
191
199
|
setPermissionDispatcher(dispatcher) {
|
|
192
200
|
permissionDispatcher = dispatcher;
|
|
201
|
+
},
|
|
202
|
+
setStartWorkDispatcher(dispatcher) {
|
|
203
|
+
startWorkDispatcher = dispatcher;
|
|
193
204
|
}
|
|
194
205
|
};
|
|
195
206
|
}
|
|
@@ -471,6 +482,7 @@ async function handleNormalizedPermission(permission, ctx) {
|
|
|
471
482
|
const pending = {
|
|
472
483
|
requestID: permission.requestID,
|
|
473
484
|
sessionID: permission.sessionID,
|
|
485
|
+
serverUrl: ctx.serverUrl.href,
|
|
474
486
|
title: permission.title,
|
|
475
487
|
permission: permission.permission,
|
|
476
488
|
patterns: permission.patterns,
|
|
@@ -514,7 +526,13 @@ function createPermissionDispatcher(ctx) {
|
|
|
514
526
|
return;
|
|
515
527
|
}
|
|
516
528
|
try {
|
|
517
|
-
await ctx.replyToPermission(
|
|
529
|
+
await ctx.replyToPermission(
|
|
530
|
+
pending.requestID,
|
|
531
|
+
pending.sessionID,
|
|
532
|
+
reply,
|
|
533
|
+
pending.endpoint,
|
|
534
|
+
pending.serverUrl
|
|
535
|
+
);
|
|
518
536
|
await ctx.bot.editMessageRemoveKeyboard(messageId, `\u2705 Permission ${replyLabel(reply)}
|
|
519
537
|
|
|
520
538
|
${pending.permission}: ${pending.title}`);
|
|
@@ -719,7 +737,7 @@ async function completeIfReady(ctx, pending, shortHash) {
|
|
|
719
737
|
const answers = pending.answersInProgress.map((answer) => answer ?? []);
|
|
720
738
|
const messageId = pending.telegramMessageIds[0];
|
|
721
739
|
try {
|
|
722
|
-
await ctx.replyToQuestion(pending.requestID, answers);
|
|
740
|
+
await ctx.replyToQuestion(pending.requestID, answers, pending.serverUrl);
|
|
723
741
|
await ctx.bot.editMessageRemoveKeyboard(
|
|
724
742
|
messageId,
|
|
725
743
|
`\u2705 Answered:
|
|
@@ -759,6 +777,7 @@ async function handleQuestionAsked(event, ctx) {
|
|
|
759
777
|
const pending = {
|
|
760
778
|
requestID: request.id,
|
|
761
779
|
sessionID: request.sessionID,
|
|
780
|
+
serverUrl: ctx.serverUrl.href,
|
|
762
781
|
questions: request.questions,
|
|
763
782
|
sentAt,
|
|
764
783
|
expiresAt: sentAt + QUESTION_EXPIRY_MS,
|
|
@@ -942,6 +961,163 @@ async function handleSessionError(event, ctx) {
|
|
|
942
961
|
ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
|
|
943
962
|
}
|
|
944
963
|
|
|
964
|
+
// src/lib/pending-start-work.ts
|
|
965
|
+
import { createHash as createHash4 } from "crypto";
|
|
966
|
+
import { mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
967
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
968
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
969
|
+
function hasCode4(err, code) {
|
|
970
|
+
return "code" in err && err.code === code;
|
|
971
|
+
}
|
|
972
|
+
function pendingFilePath3(dir, shortHash) {
|
|
973
|
+
return join4(dir, `${shortHash}.json`);
|
|
974
|
+
}
|
|
975
|
+
function parsePending3(text) {
|
|
976
|
+
const parsed = JSON.parse(text);
|
|
977
|
+
if (typeof parsed.sessionID !== "string")
|
|
978
|
+
throw new Error("Invalid pending start-work: sessionID");
|
|
979
|
+
if (parsed.serverUrl !== void 0 && typeof parsed.serverUrl !== "string")
|
|
980
|
+
throw new Error("Invalid pending start-work: serverUrl");
|
|
981
|
+
if (parsed.title !== void 0 && typeof parsed.title !== "string")
|
|
982
|
+
throw new Error("Invalid pending start-work: title");
|
|
983
|
+
if (typeof parsed.sentAt !== "number") throw new Error("Invalid pending start-work: sentAt");
|
|
984
|
+
if (typeof parsed.expiresAt !== "number")
|
|
985
|
+
throw new Error("Invalid pending start-work: expiresAt");
|
|
986
|
+
if (typeof parsed.telegramMessageId !== "number")
|
|
987
|
+
throw new Error("Invalid pending start-work: telegramMessageId");
|
|
988
|
+
return parsed;
|
|
989
|
+
}
|
|
990
|
+
async function listPendingFiles3(dir) {
|
|
991
|
+
try {
|
|
992
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
993
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
|
|
996
|
+
throw err;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function shortHashFromFileName3(fileName) {
|
|
1000
|
+
return fileName.slice(0, -".json".length);
|
|
1001
|
+
}
|
|
1002
|
+
function createPendingStartWorkStore(opts) {
|
|
1003
|
+
const dir = opts.baseDir ?? join4(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
|
|
1004
|
+
return {
|
|
1005
|
+
dir,
|
|
1006
|
+
async savePending(shortHash, data) {
|
|
1007
|
+
const filePath = pendingFilePath3(dir, shortHash);
|
|
1008
|
+
await mkdir4(dirname3(filePath), { recursive: true });
|
|
1009
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1010
|
+
await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
1011
|
+
await rename3(tmpPath, filePath);
|
|
1012
|
+
},
|
|
1013
|
+
async loadPending(shortHash) {
|
|
1014
|
+
try {
|
|
1015
|
+
return parsePending3(await readFile3(pendingFilePath3(dir, shortHash), "utf8"));
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
|
|
1018
|
+
throw err;
|
|
1019
|
+
}
|
|
1020
|
+
},
|
|
1021
|
+
async deletePending(shortHash) {
|
|
1022
|
+
try {
|
|
1023
|
+
await unlink4(pendingFilePath3(dir, shortHash));
|
|
1024
|
+
} catch (err) {
|
|
1025
|
+
if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
|
|
1026
|
+
}
|
|
1027
|
+
},
|
|
1028
|
+
async sweepExpired() {
|
|
1029
|
+
const expired = [];
|
|
1030
|
+
for (const fileName of await listPendingFiles3(dir)) {
|
|
1031
|
+
const shortHash = shortHashFromFileName3(fileName);
|
|
1032
|
+
const data = await this.loadPending(shortHash);
|
|
1033
|
+
if (data && data.expiresAt < Date.now()) {
|
|
1034
|
+
expired.push(data);
|
|
1035
|
+
await this.deletePending(shortHash);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return expired;
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
function createStartWorkShortHash(sessionID) {
|
|
1043
|
+
return createHash4("sha256").update(sessionID).digest("base64url").slice(0, 10);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/events/start-work.ts
|
|
1047
|
+
var CALLBACK_RE3 = /^sw:([^:]+)$/;
|
|
1048
|
+
var START_WORK_COMMAND = "start-work";
|
|
1049
|
+
var START_WORK_EXPIRY_MS = 24 * 60 * 6e4;
|
|
1050
|
+
function startWorkKeyboard(shortHash) {
|
|
1051
|
+
const callbackData = `sw:${shortHash}`;
|
|
1052
|
+
if (Buffer.byteLength(callbackData, "utf8") > 64)
|
|
1053
|
+
throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
1054
|
+
return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
|
|
1055
|
+
}
|
|
1056
|
+
function planCompleteMessage(title) {
|
|
1057
|
+
return title ? `plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.
|
|
1058
|
+
|
|
1059
|
+
${title}` : "plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.";
|
|
1060
|
+
}
|
|
1061
|
+
function createPendingStartWork(sessionID, title, serverUrl, telegramMessageId) {
|
|
1062
|
+
const sentAt = Date.now();
|
|
1063
|
+
return {
|
|
1064
|
+
sessionID,
|
|
1065
|
+
serverUrl,
|
|
1066
|
+
title: title ?? void 0,
|
|
1067
|
+
sentAt,
|
|
1068
|
+
expiresAt: sentAt + START_WORK_EXPIRY_MS,
|
|
1069
|
+
telegramMessageId
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function startWorkShortHash(sessionID) {
|
|
1073
|
+
return createStartWorkShortHash(sessionID);
|
|
1074
|
+
}
|
|
1075
|
+
async function expirePending3(ctx, shortHash, pending, messageId) {
|
|
1076
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 /start-work request expired");
|
|
1077
|
+
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1078
|
+
ctx.logger.info("pending start-work expired", { sessionID: pending.sessionID });
|
|
1079
|
+
}
|
|
1080
|
+
function createStartWorkDispatcher(ctx) {
|
|
1081
|
+
return {
|
|
1082
|
+
async handleCallbackQuery(data, messageId) {
|
|
1083
|
+
const match = CALLBACK_RE3.exec(data);
|
|
1084
|
+
if (!match) return;
|
|
1085
|
+
const shortHash = match[1];
|
|
1086
|
+
const pending = await ctx.pendingStartWorks.loadPending(shortHash);
|
|
1087
|
+
if (!pending) {
|
|
1088
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "This /start-work request has expired.");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
if (pending.expiresAt < Date.now()) {
|
|
1092
|
+
await expirePending3(ctx, shortHash, pending, messageId);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
await ctx.runSessionCommand(pending.sessionID, START_WORK_COMMAND, pending.serverUrl);
|
|
1097
|
+
const label = pending.title ?? pending.sessionID;
|
|
1098
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1099
|
+
messageId,
|
|
1100
|
+
`\u25B6\uFE0F Sent /start-work to opencode.
|
|
1101
|
+
|
|
1102
|
+
Session: ${label}`
|
|
1103
|
+
);
|
|
1104
|
+
ctx.logger.info("start-work command sent", { sessionID: pending.sessionID });
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1107
|
+
messageId,
|
|
1108
|
+
"\u26A0\uFE0F Failed to send /start-work to opencode"
|
|
1109
|
+
);
|
|
1110
|
+
ctx.logger.error("failed to send start-work command", {
|
|
1111
|
+
sessionID: pending.sessionID,
|
|
1112
|
+
error: String(err)
|
|
1113
|
+
});
|
|
1114
|
+
} finally {
|
|
1115
|
+
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
945
1121
|
// src/events/session-idle.ts
|
|
946
1122
|
var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
|
|
947
1123
|
function sleep(ms) {
|
|
@@ -988,9 +1164,21 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
988
1164
|
});
|
|
989
1165
|
if (!claimed) return;
|
|
990
1166
|
const title = ctx.sessionTitleService.getSessionTitle(sessionId);
|
|
991
|
-
const
|
|
1167
|
+
const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
|
|
1168
|
+
const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
|
|
992
1169
|
try {
|
|
993
|
-
|
|
1170
|
+
if (isPlanSession) {
|
|
1171
|
+
const shortHash = startWorkShortHash(sessionId);
|
|
1172
|
+
const message = await ctx.bot.sendMessage(text, {
|
|
1173
|
+
reply_markup: { inline_keyboard: startWorkKeyboard(shortHash) }
|
|
1174
|
+
});
|
|
1175
|
+
await ctx.pendingStartWorks.savePending(
|
|
1176
|
+
shortHash,
|
|
1177
|
+
createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id)
|
|
1178
|
+
);
|
|
1179
|
+
} else {
|
|
1180
|
+
await ctx.bot.sendMessage(text);
|
|
1181
|
+
}
|
|
994
1182
|
ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
|
|
995
1183
|
ctx.logger.info("idle notification sent", { sessionId, title });
|
|
996
1184
|
} catch (err) {
|
|
@@ -1000,15 +1188,19 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
1000
1188
|
async function flushDeferredParentIfReady(parentID, ctx) {
|
|
1001
1189
|
if (!ctx.sessionTitleService.hasDeferredIdleNotification(parentID)) return;
|
|
1002
1190
|
if (ctx.sessionTitleService.hasUnfinishedDescendants(parentID)) return;
|
|
1003
|
-
|
|
1191
|
+
const parentStatus = ctx.sessionTitleService.getSessionStatus(parentID);
|
|
1192
|
+
if (parentStatus === "idle") {
|
|
1193
|
+
ctx.logger.info("keeping deferred parent idle notification - waiting for parent to resume", {
|
|
1194
|
+
sessionId: parentID
|
|
1195
|
+
});
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (parentStatus !== void 0) {
|
|
1004
1199
|
ctx.sessionTitleService.clearDeferredIdleNotification(parentID);
|
|
1005
1200
|
ctx.logger.info("clearing deferred parent idle notification - parent resumed", {
|
|
1006
1201
|
sessionId: parentID
|
|
1007
1202
|
});
|
|
1008
|
-
return;
|
|
1009
1203
|
}
|
|
1010
|
-
ctx.logger.info("sending deferred parent idle notification", { sessionId: parentID });
|
|
1011
|
-
await sendIdleNotification(parentID, ctx);
|
|
1012
1204
|
}
|
|
1013
1205
|
async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
|
|
1014
1206
|
await hydrateDescendants(sessionId, ctx);
|
|
@@ -1021,8 +1213,8 @@ async function deferParentIdleIfDescendantsRunning(sessionId, ctx) {
|
|
|
1021
1213
|
}
|
|
1022
1214
|
async function handleSessionIdle(event, ctx) {
|
|
1023
1215
|
const sessionId = event.properties.sessionID;
|
|
1024
|
-
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1025
1216
|
const parentID = await resolveParentID(sessionId, ctx);
|
|
1217
|
+
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1026
1218
|
if (typeof parentID === "string") {
|
|
1027
1219
|
ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
|
|
1028
1220
|
await flushDeferredParentIfReady(parentID, ctx);
|
|
@@ -1064,14 +1256,14 @@ async function handleSessionUpdated(event, ctx) {
|
|
|
1064
1256
|
// src/lib/env-loader.ts
|
|
1065
1257
|
import { existsSync } from "fs";
|
|
1066
1258
|
import { homedir } from "os";
|
|
1067
|
-
import { join as
|
|
1259
|
+
import { join as join5 } from "path";
|
|
1068
1260
|
import dotenv from "dotenv";
|
|
1069
1261
|
function loadPluginEnv(opts) {
|
|
1070
1262
|
const paths = [
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1263
|
+
join5(opts.pluginDir, "../../.env"),
|
|
1264
|
+
join5(opts.pluginDir, "..", ".env"),
|
|
1265
|
+
join5(opts.pluginDir, ".env"),
|
|
1266
|
+
join5(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
|
|
1075
1267
|
];
|
|
1076
1268
|
const loadedFrom = [];
|
|
1077
1269
|
const values = {};
|
|
@@ -1089,10 +1281,10 @@ function loadPluginEnv(opts) {
|
|
|
1089
1281
|
}
|
|
1090
1282
|
|
|
1091
1283
|
// src/lib/lock.ts
|
|
1092
|
-
import { open as open2, readFile as
|
|
1284
|
+
import { open as open2, readFile as readFile4, stat as stat2, unlink as unlink5 } from "fs/promises";
|
|
1093
1285
|
import { hostname } from "os";
|
|
1094
1286
|
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
1095
|
-
function
|
|
1287
|
+
function hasCode5(err, code) {
|
|
1096
1288
|
return "code" in err && err.code === code;
|
|
1097
1289
|
}
|
|
1098
1290
|
function parseLockData(text) {
|
|
@@ -1111,7 +1303,7 @@ function isPidAlive(pid) {
|
|
|
1111
1303
|
process.kill(pid, 0);
|
|
1112
1304
|
return true;
|
|
1113
1305
|
} catch (err) {
|
|
1114
|
-
if (err instanceof Error &&
|
|
1306
|
+
if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
|
|
1115
1307
|
return true;
|
|
1116
1308
|
}
|
|
1117
1309
|
}
|
|
@@ -1132,7 +1324,7 @@ async function createLock(lockPath, pid) {
|
|
|
1132
1324
|
if (released) return;
|
|
1133
1325
|
released = true;
|
|
1134
1326
|
try {
|
|
1135
|
-
await
|
|
1327
|
+
await unlink5(lockPath);
|
|
1136
1328
|
} catch {
|
|
1137
1329
|
}
|
|
1138
1330
|
}
|
|
@@ -1142,7 +1334,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1142
1334
|
let ownerPid;
|
|
1143
1335
|
let dead = false;
|
|
1144
1336
|
try {
|
|
1145
|
-
const text = await
|
|
1337
|
+
const text = await readFile4(lockPath, "utf8");
|
|
1146
1338
|
const data = parseLockData(text);
|
|
1147
1339
|
if (data) {
|
|
1148
1340
|
ownerPid = data.pid;
|
|
@@ -1168,7 +1360,7 @@ async function acquireLock(opts) {
|
|
|
1168
1360
|
try {
|
|
1169
1361
|
return { acquired: true, handle: await createLock(opts.lockPath, pid) };
|
|
1170
1362
|
} catch (err) {
|
|
1171
|
-
if (!(err instanceof Error) || !
|
|
1363
|
+
if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
|
|
1172
1364
|
return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1173
1365
|
}
|
|
1174
1366
|
const existing = await inspectExisting(opts.lockPath, ttlMs);
|
|
@@ -1176,7 +1368,7 @@ async function acquireLock(opts) {
|
|
|
1176
1368
|
return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
|
|
1177
1369
|
}
|
|
1178
1370
|
try {
|
|
1179
|
-
await
|
|
1371
|
+
await unlink5(opts.lockPath);
|
|
1180
1372
|
} catch {
|
|
1181
1373
|
return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
|
|
1182
1374
|
}
|
|
@@ -1187,7 +1379,7 @@ async function acquireLock(opts) {
|
|
|
1187
1379
|
|
|
1188
1380
|
// src/lib/logger.ts
|
|
1189
1381
|
import { appendFile } from "fs/promises";
|
|
1190
|
-
import { tmpdir as
|
|
1382
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1191
1383
|
var DEFAULT_BUFFER_LIMIT = 4096;
|
|
1192
1384
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
1193
1385
|
function safeJson(data) {
|
|
@@ -1198,7 +1390,7 @@ function safeJson(data) {
|
|
|
1198
1390
|
}
|
|
1199
1391
|
}
|
|
1200
1392
|
function createLogger(opts = {}) {
|
|
1201
|
-
const filePath = opts.filePath ?? `${
|
|
1393
|
+
const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
|
|
1202
1394
|
const namespace = opts.namespace ?? "default";
|
|
1203
1395
|
const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
|
|
1204
1396
|
const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
@@ -1256,10 +1448,10 @@ function createLogger(opts = {}) {
|
|
|
1256
1448
|
}
|
|
1257
1449
|
|
|
1258
1450
|
// src/lib/state-store.ts
|
|
1259
|
-
import { mkdir as
|
|
1451
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
1260
1452
|
import { homedir as homedir2 } from "os";
|
|
1261
|
-
import { dirname as
|
|
1262
|
-
function
|
|
1453
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1454
|
+
function hasCode6(err, code) {
|
|
1263
1455
|
return "code" in err && err.code === code;
|
|
1264
1456
|
}
|
|
1265
1457
|
function parseState(text) {
|
|
@@ -1271,28 +1463,28 @@ function parseState(text) {
|
|
|
1271
1463
|
return state;
|
|
1272
1464
|
}
|
|
1273
1465
|
function createStateStore(opts = {}) {
|
|
1274
|
-
const filePath = opts.filePath ??
|
|
1466
|
+
const filePath = opts.filePath ?? join6(homedir2(), ".config/opencode/telegram-remote/state.json");
|
|
1275
1467
|
return {
|
|
1276
1468
|
async read() {
|
|
1277
1469
|
try {
|
|
1278
|
-
return parseState(await
|
|
1470
|
+
return parseState(await readFile5(filePath, "utf8"));
|
|
1279
1471
|
} catch (err) {
|
|
1280
|
-
if (err instanceof Error &&
|
|
1472
|
+
if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
|
|
1281
1473
|
throw err;
|
|
1282
1474
|
}
|
|
1283
1475
|
},
|
|
1284
1476
|
async write(patch) {
|
|
1285
1477
|
const existing = await this.read();
|
|
1286
1478
|
const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1287
|
-
await
|
|
1479
|
+
await mkdir5(dirname4(filePath), { recursive: true });
|
|
1288
1480
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1289
|
-
await
|
|
1481
|
+
await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1290
1482
|
try {
|
|
1291
|
-
await
|
|
1483
|
+
await rename4(tmpPath, filePath);
|
|
1292
1484
|
} catch (err) {
|
|
1293
|
-
if (!(err instanceof Error) || !
|
|
1294
|
-
await
|
|
1295
|
-
await
|
|
1485
|
+
if (!(err instanceof Error) || !hasCode6(err, "ENOENT")) throw err;
|
|
1486
|
+
await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1487
|
+
await rename4(tmpPath, filePath);
|
|
1296
1488
|
}
|
|
1297
1489
|
return next;
|
|
1298
1490
|
}
|
|
@@ -1300,6 +1492,10 @@ function createStateStore(opts = {}) {
|
|
|
1300
1492
|
}
|
|
1301
1493
|
|
|
1302
1494
|
// src/services/session-title-service.ts
|
|
1495
|
+
function agentFromSession(info) {
|
|
1496
|
+
const candidate = info;
|
|
1497
|
+
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
1498
|
+
}
|
|
1303
1499
|
var SessionTitleService = class {
|
|
1304
1500
|
sessions = /* @__PURE__ */ new Map();
|
|
1305
1501
|
setSessionInfo(info) {
|
|
@@ -1307,6 +1503,7 @@ var SessionTitleService = class {
|
|
|
1307
1503
|
this.sessions.set(info.id, {
|
|
1308
1504
|
title: info.title || null,
|
|
1309
1505
|
parentID: info.parentID ?? null,
|
|
1506
|
+
agent: agentFromSession(info) ?? existing?.agent,
|
|
1310
1507
|
status: existing?.status,
|
|
1311
1508
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1312
1509
|
});
|
|
@@ -1315,7 +1512,18 @@ var SessionTitleService = class {
|
|
|
1315
1512
|
const existing = this.sessions.get(sessionId);
|
|
1316
1513
|
this.sessions.set(sessionId, {
|
|
1317
1514
|
title,
|
|
1318
|
-
parentID: existing?.parentID
|
|
1515
|
+
parentID: existing?.parentID,
|
|
1516
|
+
agent: existing?.agent,
|
|
1517
|
+
status: existing?.status,
|
|
1518
|
+
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
setSessionAgent(sessionId, agent) {
|
|
1522
|
+
const existing = this.sessions.get(sessionId);
|
|
1523
|
+
this.sessions.set(sessionId, {
|
|
1524
|
+
title: existing?.title ?? null,
|
|
1525
|
+
parentID: existing?.parentID,
|
|
1526
|
+
agent,
|
|
1319
1527
|
status: existing?.status,
|
|
1320
1528
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1321
1529
|
});
|
|
@@ -1324,7 +1532,8 @@ var SessionTitleService = class {
|
|
|
1324
1532
|
const existing = this.sessions.get(sessionId);
|
|
1325
1533
|
this.sessions.set(sessionId, {
|
|
1326
1534
|
title: existing?.title ?? null,
|
|
1327
|
-
parentID: existing?.parentID
|
|
1535
|
+
parentID: existing?.parentID,
|
|
1536
|
+
agent: existing?.agent,
|
|
1328
1537
|
status,
|
|
1329
1538
|
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
|
|
1330
1539
|
});
|
|
@@ -1335,6 +1544,9 @@ var SessionTitleService = class {
|
|
|
1335
1544
|
getParentID(sessionId) {
|
|
1336
1545
|
return this.sessions.get(sessionId)?.parentID;
|
|
1337
1546
|
}
|
|
1547
|
+
getSessionAgent(sessionId) {
|
|
1548
|
+
return this.sessions.get(sessionId)?.agent;
|
|
1549
|
+
}
|
|
1338
1550
|
getSessionStatus(sessionId) {
|
|
1339
1551
|
return this.sessions.get(sessionId)?.status;
|
|
1340
1552
|
}
|
|
@@ -1350,7 +1562,8 @@ var SessionTitleService = class {
|
|
|
1350
1562
|
const existing = this.sessions.get(sessionId);
|
|
1351
1563
|
this.sessions.set(sessionId, {
|
|
1352
1564
|
title: existing?.title ?? null,
|
|
1353
|
-
parentID: existing?.parentID
|
|
1565
|
+
parentID: existing?.parentID,
|
|
1566
|
+
agent: existing?.agent,
|
|
1354
1567
|
status: existing?.status ?? "idle",
|
|
1355
1568
|
idleNotificationPending: true
|
|
1356
1569
|
});
|
|
@@ -1369,7 +1582,33 @@ var SessionTitleService = class {
|
|
|
1369
1582
|
};
|
|
1370
1583
|
|
|
1371
1584
|
// src/telegram-remote.ts
|
|
1372
|
-
var pluginDir =
|
|
1585
|
+
var pluginDir = dirname5(fileURLToPath(import.meta.url));
|
|
1586
|
+
async function postToServer(serverUrl, path, body) {
|
|
1587
|
+
const url = new URL(path, serverUrl);
|
|
1588
|
+
const response = await fetch(url, {
|
|
1589
|
+
method: "POST",
|
|
1590
|
+
headers: { "Content-Type": "application/json" },
|
|
1591
|
+
body: JSON.stringify(body)
|
|
1592
|
+
});
|
|
1593
|
+
if (response.ok) return;
|
|
1594
|
+
const text = await response.text();
|
|
1595
|
+
throw new Error(
|
|
1596
|
+
`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
function getSessionAgentFromMessage(event) {
|
|
1600
|
+
const info = event.properties.info;
|
|
1601
|
+
if (info.role !== "user") return void 0;
|
|
1602
|
+
return { sessionID: info.sessionID, agent: info.agent };
|
|
1603
|
+
}
|
|
1604
|
+
function getSessionAgentFromNextStep(event) {
|
|
1605
|
+
if (event.type !== "session.next.step.started") return void 0;
|
|
1606
|
+
const props = event.properties;
|
|
1607
|
+
if (!props) return void 0;
|
|
1608
|
+
if (typeof props.sessionID !== "string") return void 0;
|
|
1609
|
+
if (typeof props.agent !== "string") return void 0;
|
|
1610
|
+
return { sessionID: props.sessionID, agent: props.agent };
|
|
1611
|
+
}
|
|
1373
1612
|
var TelegramRemote = async (input) => {
|
|
1374
1613
|
const logger = createLogger({ namespace: "telegram" });
|
|
1375
1614
|
try {
|
|
@@ -1378,11 +1617,12 @@ var TelegramRemote = async (input) => {
|
|
|
1378
1617
|
const config = loadConfig({ logger, env: process.env });
|
|
1379
1618
|
const stateStore = createStateStore();
|
|
1380
1619
|
const initialState = await stateStore.read();
|
|
1381
|
-
const tokenHash =
|
|
1382
|
-
const lockPath =
|
|
1383
|
-
const claimsDir =
|
|
1620
|
+
const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
|
|
1621
|
+
const lockPath = join7(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
|
|
1622
|
+
const claimsDir = join7(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
|
|
1384
1623
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1385
1624
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1625
|
+
const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
|
|
1386
1626
|
const lockResult = await acquireLock({ lockPath });
|
|
1387
1627
|
const isLeader = lockResult.acquired;
|
|
1388
1628
|
logger.info(
|
|
@@ -1396,31 +1636,59 @@ var TelegramRemote = async (input) => {
|
|
|
1396
1636
|
});
|
|
1397
1637
|
const sessionTitleService = new SessionTitleService();
|
|
1398
1638
|
const client = input.client;
|
|
1399
|
-
const replyToQuestion = async (requestID, answers) => {
|
|
1639
|
+
const replyToQuestion = async (requestID, answers, serverUrl = input.serverUrl.href) => {
|
|
1640
|
+
const path = `/question/${encodeURIComponent(requestID)}/reply`;
|
|
1641
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1642
|
+
await postToServer(serverUrl, path, { answers });
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1400
1645
|
await client._client.post({
|
|
1401
|
-
url:
|
|
1646
|
+
url: path,
|
|
1402
1647
|
headers: { "Content-Type": "application/json" },
|
|
1403
1648
|
body: { answers },
|
|
1404
1649
|
throwOnError: true
|
|
1405
1650
|
});
|
|
1406
1651
|
};
|
|
1407
|
-
const replyToPermission = async (requestID, sessionID, reply, endpoint) => {
|
|
1652
|
+
const replyToPermission = async (requestID, sessionID, reply, endpoint, serverUrl = input.serverUrl.href) => {
|
|
1408
1653
|
if (endpoint === "request") {
|
|
1654
|
+
const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
|
|
1655
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1656
|
+
await postToServer(serverUrl, path2, { reply });
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1409
1659
|
await client._client.post({
|
|
1410
|
-
url:
|
|
1660
|
+
url: path2,
|
|
1411
1661
|
headers: { "Content-Type": "application/json" },
|
|
1412
1662
|
body: { reply },
|
|
1413
1663
|
throwOnError: true
|
|
1414
1664
|
});
|
|
1415
1665
|
return;
|
|
1416
1666
|
}
|
|
1667
|
+
const path = `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(requestID)}`;
|
|
1668
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1669
|
+
await postToServer(serverUrl, path, { response: reply });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1417
1672
|
await client._client.post({
|
|
1418
|
-
url:
|
|
1673
|
+
url: path,
|
|
1419
1674
|
headers: { "Content-Type": "application/json" },
|
|
1420
1675
|
body: { response: reply },
|
|
1421
1676
|
throwOnError: true
|
|
1422
1677
|
});
|
|
1423
1678
|
};
|
|
1679
|
+
const runSessionCommand = async (sessionID, command, serverUrl = input.serverUrl.href) => {
|
|
1680
|
+
const path = `/session/${encodeURIComponent(sessionID)}/command`;
|
|
1681
|
+
const body = { command, arguments: "" };
|
|
1682
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1683
|
+
await postToServer(serverUrl, path, body);
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
await input.client.session.command({
|
|
1687
|
+
path: { id: sessionID },
|
|
1688
|
+
body,
|
|
1689
|
+
throwOnError: true
|
|
1690
|
+
});
|
|
1691
|
+
};
|
|
1424
1692
|
const bot = createTelegramBot({
|
|
1425
1693
|
config,
|
|
1426
1694
|
stateStore,
|
|
@@ -1465,12 +1733,15 @@ var TelegramRemote = async (input) => {
|
|
|
1465
1733
|
tokenHash,
|
|
1466
1734
|
pendingQuestions,
|
|
1467
1735
|
pendingPermissions,
|
|
1736
|
+
pendingStartWorks,
|
|
1468
1737
|
replyToQuestion,
|
|
1469
|
-
replyToPermission
|
|
1738
|
+
replyToPermission,
|
|
1739
|
+
runSessionCommand
|
|
1470
1740
|
};
|
|
1471
1741
|
if (isLeader) {
|
|
1472
1742
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1473
1743
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1744
|
+
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
1474
1745
|
}
|
|
1475
1746
|
return {
|
|
1476
1747
|
event: async ({ event }) => {
|
|
@@ -1485,9 +1756,20 @@ var TelegramRemote = async (input) => {
|
|
|
1485
1756
|
return handleSessionCreated(event, ctx);
|
|
1486
1757
|
case "session.updated":
|
|
1487
1758
|
return handleSessionUpdated(event, ctx);
|
|
1759
|
+
case "message.updated": {
|
|
1760
|
+
const messageAgent = getSessionAgentFromMessage(event);
|
|
1761
|
+
if (messageAgent)
|
|
1762
|
+
ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1488
1765
|
case "permission.updated":
|
|
1489
1766
|
return handlePermissionUpdated(event, ctx);
|
|
1490
1767
|
default: {
|
|
1768
|
+
const stepAgent = getSessionAgentFromNextStep(extEvent);
|
|
1769
|
+
if (stepAgent) {
|
|
1770
|
+
ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1491
1773
|
if (isEventPermissionAsked(extEvent)) {
|
|
1492
1774
|
if (!isLeader) return;
|
|
1493
1775
|
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.11",
|
|
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",
|