@coinseeker/opencode-telegram-plugin 1.0.10 → 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 +281 -37
- 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
|
}
|
|
@@ -950,6 +961,163 @@ async function handleSessionError(event, ctx) {
|
|
|
950
961
|
ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
|
|
951
962
|
}
|
|
952
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
|
+
|
|
953
1121
|
// src/events/session-idle.ts
|
|
954
1122
|
var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
|
|
955
1123
|
function sleep(ms) {
|
|
@@ -996,9 +1164,21 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
996
1164
|
});
|
|
997
1165
|
if (!claimed) return;
|
|
998
1166
|
const title = ctx.sessionTitleService.getSessionTitle(sessionId);
|
|
999
|
-
const
|
|
1167
|
+
const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
|
|
1168
|
+
const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
|
|
1000
1169
|
try {
|
|
1001
|
-
|
|
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
|
+
}
|
|
1002
1182
|
ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
|
|
1003
1183
|
ctx.logger.info("idle notification sent", { sessionId, title });
|
|
1004
1184
|
} catch (err) {
|
|
@@ -1076,14 +1256,14 @@ async function handleSessionUpdated(event, ctx) {
|
|
|
1076
1256
|
// src/lib/env-loader.ts
|
|
1077
1257
|
import { existsSync } from "fs";
|
|
1078
1258
|
import { homedir } from "os";
|
|
1079
|
-
import { join as
|
|
1259
|
+
import { join as join5 } from "path";
|
|
1080
1260
|
import dotenv from "dotenv";
|
|
1081
1261
|
function loadPluginEnv(opts) {
|
|
1082
1262
|
const paths = [
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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")
|
|
1087
1267
|
];
|
|
1088
1268
|
const loadedFrom = [];
|
|
1089
1269
|
const values = {};
|
|
@@ -1101,10 +1281,10 @@ function loadPluginEnv(opts) {
|
|
|
1101
1281
|
}
|
|
1102
1282
|
|
|
1103
1283
|
// src/lib/lock.ts
|
|
1104
|
-
import { open as open2, readFile as
|
|
1284
|
+
import { open as open2, readFile as readFile4, stat as stat2, unlink as unlink5 } from "fs/promises";
|
|
1105
1285
|
import { hostname } from "os";
|
|
1106
1286
|
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
1107
|
-
function
|
|
1287
|
+
function hasCode5(err, code) {
|
|
1108
1288
|
return "code" in err && err.code === code;
|
|
1109
1289
|
}
|
|
1110
1290
|
function parseLockData(text) {
|
|
@@ -1123,7 +1303,7 @@ function isPidAlive(pid) {
|
|
|
1123
1303
|
process.kill(pid, 0);
|
|
1124
1304
|
return true;
|
|
1125
1305
|
} catch (err) {
|
|
1126
|
-
if (err instanceof Error &&
|
|
1306
|
+
if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
|
|
1127
1307
|
return true;
|
|
1128
1308
|
}
|
|
1129
1309
|
}
|
|
@@ -1144,7 +1324,7 @@ async function createLock(lockPath, pid) {
|
|
|
1144
1324
|
if (released) return;
|
|
1145
1325
|
released = true;
|
|
1146
1326
|
try {
|
|
1147
|
-
await
|
|
1327
|
+
await unlink5(lockPath);
|
|
1148
1328
|
} catch {
|
|
1149
1329
|
}
|
|
1150
1330
|
}
|
|
@@ -1154,7 +1334,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1154
1334
|
let ownerPid;
|
|
1155
1335
|
let dead = false;
|
|
1156
1336
|
try {
|
|
1157
|
-
const text = await
|
|
1337
|
+
const text = await readFile4(lockPath, "utf8");
|
|
1158
1338
|
const data = parseLockData(text);
|
|
1159
1339
|
if (data) {
|
|
1160
1340
|
ownerPid = data.pid;
|
|
@@ -1180,7 +1360,7 @@ async function acquireLock(opts) {
|
|
|
1180
1360
|
try {
|
|
1181
1361
|
return { acquired: true, handle: await createLock(opts.lockPath, pid) };
|
|
1182
1362
|
} catch (err) {
|
|
1183
|
-
if (!(err instanceof Error) || !
|
|
1363
|
+
if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
|
|
1184
1364
|
return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1185
1365
|
}
|
|
1186
1366
|
const existing = await inspectExisting(opts.lockPath, ttlMs);
|
|
@@ -1188,7 +1368,7 @@ async function acquireLock(opts) {
|
|
|
1188
1368
|
return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
|
|
1189
1369
|
}
|
|
1190
1370
|
try {
|
|
1191
|
-
await
|
|
1371
|
+
await unlink5(opts.lockPath);
|
|
1192
1372
|
} catch {
|
|
1193
1373
|
return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
|
|
1194
1374
|
}
|
|
@@ -1199,7 +1379,7 @@ async function acquireLock(opts) {
|
|
|
1199
1379
|
|
|
1200
1380
|
// src/lib/logger.ts
|
|
1201
1381
|
import { appendFile } from "fs/promises";
|
|
1202
|
-
import { tmpdir as
|
|
1382
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1203
1383
|
var DEFAULT_BUFFER_LIMIT = 4096;
|
|
1204
1384
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
1205
1385
|
function safeJson(data) {
|
|
@@ -1210,7 +1390,7 @@ function safeJson(data) {
|
|
|
1210
1390
|
}
|
|
1211
1391
|
}
|
|
1212
1392
|
function createLogger(opts = {}) {
|
|
1213
|
-
const filePath = opts.filePath ?? `${
|
|
1393
|
+
const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
|
|
1214
1394
|
const namespace = opts.namespace ?? "default";
|
|
1215
1395
|
const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
|
|
1216
1396
|
const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
@@ -1268,10 +1448,10 @@ function createLogger(opts = {}) {
|
|
|
1268
1448
|
}
|
|
1269
1449
|
|
|
1270
1450
|
// src/lib/state-store.ts
|
|
1271
|
-
import { mkdir as
|
|
1451
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
1272
1452
|
import { homedir as homedir2 } from "os";
|
|
1273
|
-
import { dirname as
|
|
1274
|
-
function
|
|
1453
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1454
|
+
function hasCode6(err, code) {
|
|
1275
1455
|
return "code" in err && err.code === code;
|
|
1276
1456
|
}
|
|
1277
1457
|
function parseState(text) {
|
|
@@ -1283,28 +1463,28 @@ function parseState(text) {
|
|
|
1283
1463
|
return state;
|
|
1284
1464
|
}
|
|
1285
1465
|
function createStateStore(opts = {}) {
|
|
1286
|
-
const filePath = opts.filePath ??
|
|
1466
|
+
const filePath = opts.filePath ?? join6(homedir2(), ".config/opencode/telegram-remote/state.json");
|
|
1287
1467
|
return {
|
|
1288
1468
|
async read() {
|
|
1289
1469
|
try {
|
|
1290
|
-
return parseState(await
|
|
1470
|
+
return parseState(await readFile5(filePath, "utf8"));
|
|
1291
1471
|
} catch (err) {
|
|
1292
|
-
if (err instanceof Error &&
|
|
1472
|
+
if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
|
|
1293
1473
|
throw err;
|
|
1294
1474
|
}
|
|
1295
1475
|
},
|
|
1296
1476
|
async write(patch) {
|
|
1297
1477
|
const existing = await this.read();
|
|
1298
1478
|
const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1299
|
-
await
|
|
1479
|
+
await mkdir5(dirname4(filePath), { recursive: true });
|
|
1300
1480
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1301
|
-
await
|
|
1481
|
+
await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1302
1482
|
try {
|
|
1303
|
-
await
|
|
1483
|
+
await rename4(tmpPath, filePath);
|
|
1304
1484
|
} catch (err) {
|
|
1305
|
-
if (!(err instanceof Error) || !
|
|
1306
|
-
await
|
|
1307
|
-
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);
|
|
1308
1488
|
}
|
|
1309
1489
|
return next;
|
|
1310
1490
|
}
|
|
@@ -1312,6 +1492,10 @@ function createStateStore(opts = {}) {
|
|
|
1312
1492
|
}
|
|
1313
1493
|
|
|
1314
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
|
+
}
|
|
1315
1499
|
var SessionTitleService = class {
|
|
1316
1500
|
sessions = /* @__PURE__ */ new Map();
|
|
1317
1501
|
setSessionInfo(info) {
|
|
@@ -1319,6 +1503,7 @@ var SessionTitleService = class {
|
|
|
1319
1503
|
this.sessions.set(info.id, {
|
|
1320
1504
|
title: info.title || null,
|
|
1321
1505
|
parentID: info.parentID ?? null,
|
|
1506
|
+
agent: agentFromSession(info) ?? existing?.agent,
|
|
1322
1507
|
status: existing?.status,
|
|
1323
1508
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1324
1509
|
});
|
|
@@ -1328,6 +1513,17 @@ var SessionTitleService = class {
|
|
|
1328
1513
|
this.sessions.set(sessionId, {
|
|
1329
1514
|
title,
|
|
1330
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,
|
|
1331
1527
|
status: existing?.status,
|
|
1332
1528
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1333
1529
|
});
|
|
@@ -1337,6 +1533,7 @@ var SessionTitleService = class {
|
|
|
1337
1533
|
this.sessions.set(sessionId, {
|
|
1338
1534
|
title: existing?.title ?? null,
|
|
1339
1535
|
parentID: existing?.parentID,
|
|
1536
|
+
agent: existing?.agent,
|
|
1340
1537
|
status,
|
|
1341
1538
|
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
|
|
1342
1539
|
});
|
|
@@ -1347,6 +1544,9 @@ var SessionTitleService = class {
|
|
|
1347
1544
|
getParentID(sessionId) {
|
|
1348
1545
|
return this.sessions.get(sessionId)?.parentID;
|
|
1349
1546
|
}
|
|
1547
|
+
getSessionAgent(sessionId) {
|
|
1548
|
+
return this.sessions.get(sessionId)?.agent;
|
|
1549
|
+
}
|
|
1350
1550
|
getSessionStatus(sessionId) {
|
|
1351
1551
|
return this.sessions.get(sessionId)?.status;
|
|
1352
1552
|
}
|
|
@@ -1363,6 +1563,7 @@ var SessionTitleService = class {
|
|
|
1363
1563
|
this.sessions.set(sessionId, {
|
|
1364
1564
|
title: existing?.title ?? null,
|
|
1365
1565
|
parentID: existing?.parentID,
|
|
1566
|
+
agent: existing?.agent,
|
|
1366
1567
|
status: existing?.status ?? "idle",
|
|
1367
1568
|
idleNotificationPending: true
|
|
1368
1569
|
});
|
|
@@ -1381,7 +1582,7 @@ var SessionTitleService = class {
|
|
|
1381
1582
|
};
|
|
1382
1583
|
|
|
1383
1584
|
// src/telegram-remote.ts
|
|
1384
|
-
var pluginDir =
|
|
1585
|
+
var pluginDir = dirname5(fileURLToPath(import.meta.url));
|
|
1385
1586
|
async function postToServer(serverUrl, path, body) {
|
|
1386
1587
|
const url = new URL(path, serverUrl);
|
|
1387
1588
|
const response = await fetch(url, {
|
|
@@ -1391,7 +1592,22 @@ async function postToServer(serverUrl, path, body) {
|
|
|
1391
1592
|
});
|
|
1392
1593
|
if (response.ok) return;
|
|
1393
1594
|
const text = await response.text();
|
|
1394
|
-
throw new Error(
|
|
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 };
|
|
1395
1611
|
}
|
|
1396
1612
|
var TelegramRemote = async (input) => {
|
|
1397
1613
|
const logger = createLogger({ namespace: "telegram" });
|
|
@@ -1401,11 +1617,12 @@ var TelegramRemote = async (input) => {
|
|
|
1401
1617
|
const config = loadConfig({ logger, env: process.env });
|
|
1402
1618
|
const stateStore = createStateStore();
|
|
1403
1619
|
const initialState = await stateStore.read();
|
|
1404
|
-
const tokenHash =
|
|
1405
|
-
const lockPath =
|
|
1406
|
-
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}`);
|
|
1407
1623
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1408
1624
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1625
|
+
const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
|
|
1409
1626
|
const lockResult = await acquireLock({ lockPath });
|
|
1410
1627
|
const isLeader = lockResult.acquired;
|
|
1411
1628
|
logger.info(
|
|
@@ -1459,6 +1676,19 @@ var TelegramRemote = async (input) => {
|
|
|
1459
1676
|
throwOnError: true
|
|
1460
1677
|
});
|
|
1461
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
|
+
};
|
|
1462
1692
|
const bot = createTelegramBot({
|
|
1463
1693
|
config,
|
|
1464
1694
|
stateStore,
|
|
@@ -1503,12 +1733,15 @@ var TelegramRemote = async (input) => {
|
|
|
1503
1733
|
tokenHash,
|
|
1504
1734
|
pendingQuestions,
|
|
1505
1735
|
pendingPermissions,
|
|
1736
|
+
pendingStartWorks,
|
|
1506
1737
|
replyToQuestion,
|
|
1507
|
-
replyToPermission
|
|
1738
|
+
replyToPermission,
|
|
1739
|
+
runSessionCommand
|
|
1508
1740
|
};
|
|
1509
1741
|
if (isLeader) {
|
|
1510
1742
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1511
1743
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1744
|
+
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
1512
1745
|
}
|
|
1513
1746
|
return {
|
|
1514
1747
|
event: async ({ event }) => {
|
|
@@ -1523,9 +1756,20 @@ var TelegramRemote = async (input) => {
|
|
|
1523
1756
|
return handleSessionCreated(event, ctx);
|
|
1524
1757
|
case "session.updated":
|
|
1525
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
|
+
}
|
|
1526
1765
|
case "permission.updated":
|
|
1527
1766
|
return handlePermissionUpdated(event, ctx);
|
|
1528
1767
|
default: {
|
|
1768
|
+
const stepAgent = getSessionAgentFromNextStep(extEvent);
|
|
1769
|
+
if (stepAgent) {
|
|
1770
|
+
ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1529
1773
|
if (isEventPermissionAsked(extEvent)) {
|
|
1530
1774
|
if (!isLeader) return;
|
|
1531
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",
|