@coinseeker/opencode-telegram-plugin 1.0.10 → 1.0.12
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 +324 -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.12"]
|
|
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.12`.
|
|
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
|
}
|
|
@@ -497,6 +508,46 @@ async function handlePermissionUpdated(event, ctx) {
|
|
|
497
508
|
async function handlePermissionAsked(event, ctx) {
|
|
498
509
|
await handleNormalizedPermission(normalizeAsked(event.properties), ctx);
|
|
499
510
|
}
|
|
511
|
+
function isEventPermissionReplied(event) {
|
|
512
|
+
if (event.type !== "permission.replied") return false;
|
|
513
|
+
const props = event.properties;
|
|
514
|
+
if (!props) return false;
|
|
515
|
+
if (typeof props.sessionID !== "string") return false;
|
|
516
|
+
const hasId = typeof props.permissionID === "string" || typeof props.requestID === "string";
|
|
517
|
+
return hasId;
|
|
518
|
+
}
|
|
519
|
+
function externalReplyLabel(value) {
|
|
520
|
+
if (value === "once") return "Allowed once in opencode";
|
|
521
|
+
if (value === "always") return "Always allowed in opencode";
|
|
522
|
+
if (value === "reject") return "Rejected in opencode";
|
|
523
|
+
return "Already answered in opencode";
|
|
524
|
+
}
|
|
525
|
+
async function handlePermissionReplied(event, ctx) {
|
|
526
|
+
const requestID = event.properties.requestID ?? event.properties.permissionID;
|
|
527
|
+
if (!requestID) return;
|
|
528
|
+
const found = await ctx.pendingPermissions.findByRequestID(requestID);
|
|
529
|
+
if (!found) return;
|
|
530
|
+
const label = externalReplyLabel(event.properties.reply ?? event.properties.response);
|
|
531
|
+
try {
|
|
532
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
533
|
+
found.data.telegramMessageId,
|
|
534
|
+
`\u2705 ${label}
|
|
535
|
+
|
|
536
|
+
${found.data.permission}: ${found.data.title}`
|
|
537
|
+
);
|
|
538
|
+
ctx.logger.info("permission externally replied - cleared pending", {
|
|
539
|
+
requestID,
|
|
540
|
+
sessionID: event.properties.sessionID
|
|
541
|
+
});
|
|
542
|
+
} catch (err) {
|
|
543
|
+
ctx.logger.error("failed to edit externally replied permission", {
|
|
544
|
+
error: String(err),
|
|
545
|
+
requestID
|
|
546
|
+
});
|
|
547
|
+
} finally {
|
|
548
|
+
await ctx.pendingPermissions.deletePending(found.shortHash);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
500
551
|
function createPermissionDispatcher(ctx) {
|
|
501
552
|
return {
|
|
502
553
|
async handleCallbackQuery(data, messageId) {
|
|
@@ -950,6 +1001,163 @@ async function handleSessionError(event, ctx) {
|
|
|
950
1001
|
ctx.logger.info("session abort recorded", { sessionId: event.properties.sessionID ?? "global" });
|
|
951
1002
|
}
|
|
952
1003
|
|
|
1004
|
+
// src/lib/pending-start-work.ts
|
|
1005
|
+
import { createHash as createHash4 } from "crypto";
|
|
1006
|
+
import { mkdir as mkdir4, readdir as readdir4, readFile as readFile3, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
1007
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
1008
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1009
|
+
function hasCode4(err, code) {
|
|
1010
|
+
return "code" in err && err.code === code;
|
|
1011
|
+
}
|
|
1012
|
+
function pendingFilePath3(dir, shortHash) {
|
|
1013
|
+
return join4(dir, `${shortHash}.json`);
|
|
1014
|
+
}
|
|
1015
|
+
function parsePending3(text) {
|
|
1016
|
+
const parsed = JSON.parse(text);
|
|
1017
|
+
if (typeof parsed.sessionID !== "string")
|
|
1018
|
+
throw new Error("Invalid pending start-work: sessionID");
|
|
1019
|
+
if (parsed.serverUrl !== void 0 && typeof parsed.serverUrl !== "string")
|
|
1020
|
+
throw new Error("Invalid pending start-work: serverUrl");
|
|
1021
|
+
if (parsed.title !== void 0 && typeof parsed.title !== "string")
|
|
1022
|
+
throw new Error("Invalid pending start-work: title");
|
|
1023
|
+
if (typeof parsed.sentAt !== "number") throw new Error("Invalid pending start-work: sentAt");
|
|
1024
|
+
if (typeof parsed.expiresAt !== "number")
|
|
1025
|
+
throw new Error("Invalid pending start-work: expiresAt");
|
|
1026
|
+
if (typeof parsed.telegramMessageId !== "number")
|
|
1027
|
+
throw new Error("Invalid pending start-work: telegramMessageId");
|
|
1028
|
+
return parsed;
|
|
1029
|
+
}
|
|
1030
|
+
async function listPendingFiles3(dir) {
|
|
1031
|
+
try {
|
|
1032
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
1033
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return [];
|
|
1036
|
+
throw err;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function shortHashFromFileName3(fileName) {
|
|
1040
|
+
return fileName.slice(0, -".json".length);
|
|
1041
|
+
}
|
|
1042
|
+
function createPendingStartWorkStore(opts) {
|
|
1043
|
+
const dir = opts.baseDir ?? join4(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
|
|
1044
|
+
return {
|
|
1045
|
+
dir,
|
|
1046
|
+
async savePending(shortHash, data) {
|
|
1047
|
+
const filePath = pendingFilePath3(dir, shortHash);
|
|
1048
|
+
await mkdir4(dirname3(filePath), { recursive: true });
|
|
1049
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1050
|
+
await writeFile3(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
1051
|
+
await rename3(tmpPath, filePath);
|
|
1052
|
+
},
|
|
1053
|
+
async loadPending(shortHash) {
|
|
1054
|
+
try {
|
|
1055
|
+
return parsePending3(await readFile3(pendingFilePath3(dir, shortHash), "utf8"));
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
if (err instanceof Error && hasCode4(err, "ENOENT")) return void 0;
|
|
1058
|
+
throw err;
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
async deletePending(shortHash) {
|
|
1062
|
+
try {
|
|
1063
|
+
await unlink4(pendingFilePath3(dir, shortHash));
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
if (!(err instanceof Error) || !hasCode4(err, "ENOENT")) throw err;
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
async sweepExpired() {
|
|
1069
|
+
const expired = [];
|
|
1070
|
+
for (const fileName of await listPendingFiles3(dir)) {
|
|
1071
|
+
const shortHash = shortHashFromFileName3(fileName);
|
|
1072
|
+
const data = await this.loadPending(shortHash);
|
|
1073
|
+
if (data && data.expiresAt < Date.now()) {
|
|
1074
|
+
expired.push(data);
|
|
1075
|
+
await this.deletePending(shortHash);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return expired;
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function createStartWorkShortHash(sessionID) {
|
|
1083
|
+
return createHash4("sha256").update(sessionID).digest("base64url").slice(0, 10);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/events/start-work.ts
|
|
1087
|
+
var CALLBACK_RE3 = /^sw:([^:]+)$/;
|
|
1088
|
+
var START_WORK_COMMAND = "start-work";
|
|
1089
|
+
var START_WORK_EXPIRY_MS = 24 * 60 * 6e4;
|
|
1090
|
+
function startWorkKeyboard(shortHash) {
|
|
1091
|
+
const callbackData = `sw:${shortHash}`;
|
|
1092
|
+
if (Buffer.byteLength(callbackData, "utf8") > 64)
|
|
1093
|
+
throw new Error("Telegram callback_data exceeds 64 bytes");
|
|
1094
|
+
return [[{ text: "\u25B6\uFE0F Run /start-work", callback_data: callbackData }]];
|
|
1095
|
+
}
|
|
1096
|
+
function planCompleteMessage(title) {
|
|
1097
|
+
return title ? `plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.
|
|
1098
|
+
|
|
1099
|
+
${title}` : "plan \uC791\uC131\uC774 \uB05D\uB0AC\uC5B4\uC694.";
|
|
1100
|
+
}
|
|
1101
|
+
function createPendingStartWork(sessionID, title, serverUrl, telegramMessageId) {
|
|
1102
|
+
const sentAt = Date.now();
|
|
1103
|
+
return {
|
|
1104
|
+
sessionID,
|
|
1105
|
+
serverUrl,
|
|
1106
|
+
title: title ?? void 0,
|
|
1107
|
+
sentAt,
|
|
1108
|
+
expiresAt: sentAt + START_WORK_EXPIRY_MS,
|
|
1109
|
+
telegramMessageId
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function startWorkShortHash(sessionID) {
|
|
1113
|
+
return createStartWorkShortHash(sessionID);
|
|
1114
|
+
}
|
|
1115
|
+
async function expirePending3(ctx, shortHash, pending, messageId) {
|
|
1116
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "\u23F1 /start-work request expired");
|
|
1117
|
+
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1118
|
+
ctx.logger.info("pending start-work expired", { sessionID: pending.sessionID });
|
|
1119
|
+
}
|
|
1120
|
+
function createStartWorkDispatcher(ctx) {
|
|
1121
|
+
return {
|
|
1122
|
+
async handleCallbackQuery(data, messageId) {
|
|
1123
|
+
const match = CALLBACK_RE3.exec(data);
|
|
1124
|
+
if (!match) return;
|
|
1125
|
+
const shortHash = match[1];
|
|
1126
|
+
const pending = await ctx.pendingStartWorks.loadPending(shortHash);
|
|
1127
|
+
if (!pending) {
|
|
1128
|
+
await ctx.bot.editMessageRemoveKeyboard(messageId, "This /start-work request has expired.");
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (pending.expiresAt < Date.now()) {
|
|
1132
|
+
await expirePending3(ctx, shortHash, pending, messageId);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
await ctx.runSessionCommand(pending.sessionID, START_WORK_COMMAND, pending.serverUrl);
|
|
1137
|
+
const label = pending.title ?? pending.sessionID;
|
|
1138
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1139
|
+
messageId,
|
|
1140
|
+
`\u25B6\uFE0F Sent /start-work to opencode.
|
|
1141
|
+
|
|
1142
|
+
Session: ${label}`
|
|
1143
|
+
);
|
|
1144
|
+
ctx.logger.info("start-work command sent", { sessionID: pending.sessionID });
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
await ctx.bot.editMessageRemoveKeyboard(
|
|
1147
|
+
messageId,
|
|
1148
|
+
"\u26A0\uFE0F Failed to send /start-work to opencode"
|
|
1149
|
+
);
|
|
1150
|
+
ctx.logger.error("failed to send start-work command", {
|
|
1151
|
+
sessionID: pending.sessionID,
|
|
1152
|
+
error: String(err)
|
|
1153
|
+
});
|
|
1154
|
+
} finally {
|
|
1155
|
+
await ctx.pendingStartWorks.deletePending(shortHash);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
953
1161
|
// src/events/session-idle.ts
|
|
954
1162
|
var ROOT_IDLE_RECHECK_DELAY_MS = 2500;
|
|
955
1163
|
function sleep(ms) {
|
|
@@ -996,9 +1204,21 @@ async function sendIdleNotification(sessionId, ctx) {
|
|
|
996
1204
|
});
|
|
997
1205
|
if (!claimed) return;
|
|
998
1206
|
const title = ctx.sessionTitleService.getSessionTitle(sessionId);
|
|
999
|
-
const
|
|
1207
|
+
const isPlanSession = ctx.sessionTitleService.getSessionAgent(sessionId) === "plan";
|
|
1208
|
+
const text = isPlanSession ? planCompleteMessage(title) : title ? `Agent has finished: ${title}` : "Agent has finished.";
|
|
1000
1209
|
try {
|
|
1001
|
-
|
|
1210
|
+
if (isPlanSession) {
|
|
1211
|
+
const shortHash = startWorkShortHash(sessionId);
|
|
1212
|
+
const message = await ctx.bot.sendMessage(text, {
|
|
1213
|
+
reply_markup: { inline_keyboard: startWorkKeyboard(shortHash) }
|
|
1214
|
+
});
|
|
1215
|
+
await ctx.pendingStartWorks.savePending(
|
|
1216
|
+
shortHash,
|
|
1217
|
+
createPendingStartWork(sessionId, title, ctx.serverUrl.href, message.message_id)
|
|
1218
|
+
);
|
|
1219
|
+
} else {
|
|
1220
|
+
await ctx.bot.sendMessage(text);
|
|
1221
|
+
}
|
|
1002
1222
|
ctx.sessionTitleService.clearDeferredIdleNotification(sessionId);
|
|
1003
1223
|
ctx.logger.info("idle notification sent", { sessionId, title });
|
|
1004
1224
|
} catch (err) {
|
|
@@ -1076,14 +1296,14 @@ async function handleSessionUpdated(event, ctx) {
|
|
|
1076
1296
|
// src/lib/env-loader.ts
|
|
1077
1297
|
import { existsSync } from "fs";
|
|
1078
1298
|
import { homedir } from "os";
|
|
1079
|
-
import { join as
|
|
1299
|
+
import { join as join5 } from "path";
|
|
1080
1300
|
import dotenv from "dotenv";
|
|
1081
1301
|
function loadPluginEnv(opts) {
|
|
1082
1302
|
const paths = [
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1303
|
+
join5(opts.pluginDir, "../../.env"),
|
|
1304
|
+
join5(opts.pluginDir, "..", ".env"),
|
|
1305
|
+
join5(opts.pluginDir, ".env"),
|
|
1306
|
+
join5(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
|
|
1087
1307
|
];
|
|
1088
1308
|
const loadedFrom = [];
|
|
1089
1309
|
const values = {};
|
|
@@ -1101,10 +1321,10 @@ function loadPluginEnv(opts) {
|
|
|
1101
1321
|
}
|
|
1102
1322
|
|
|
1103
1323
|
// src/lib/lock.ts
|
|
1104
|
-
import { open as open2, readFile as
|
|
1324
|
+
import { open as open2, readFile as readFile4, stat as stat2, unlink as unlink5 } from "fs/promises";
|
|
1105
1325
|
import { hostname } from "os";
|
|
1106
1326
|
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
1107
|
-
function
|
|
1327
|
+
function hasCode5(err, code) {
|
|
1108
1328
|
return "code" in err && err.code === code;
|
|
1109
1329
|
}
|
|
1110
1330
|
function parseLockData(text) {
|
|
@@ -1123,7 +1343,7 @@ function isPidAlive(pid) {
|
|
|
1123
1343
|
process.kill(pid, 0);
|
|
1124
1344
|
return true;
|
|
1125
1345
|
} catch (err) {
|
|
1126
|
-
if (err instanceof Error &&
|
|
1346
|
+
if (err instanceof Error && hasCode5(err, "ESRCH")) return false;
|
|
1127
1347
|
return true;
|
|
1128
1348
|
}
|
|
1129
1349
|
}
|
|
@@ -1144,7 +1364,7 @@ async function createLock(lockPath, pid) {
|
|
|
1144
1364
|
if (released) return;
|
|
1145
1365
|
released = true;
|
|
1146
1366
|
try {
|
|
1147
|
-
await
|
|
1367
|
+
await unlink5(lockPath);
|
|
1148
1368
|
} catch {
|
|
1149
1369
|
}
|
|
1150
1370
|
}
|
|
@@ -1154,7 +1374,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1154
1374
|
let ownerPid;
|
|
1155
1375
|
let dead = false;
|
|
1156
1376
|
try {
|
|
1157
|
-
const text = await
|
|
1377
|
+
const text = await readFile4(lockPath, "utf8");
|
|
1158
1378
|
const data = parseLockData(text);
|
|
1159
1379
|
if (data) {
|
|
1160
1380
|
ownerPid = data.pid;
|
|
@@ -1180,7 +1400,7 @@ async function acquireLock(opts) {
|
|
|
1180
1400
|
try {
|
|
1181
1401
|
return { acquired: true, handle: await createLock(opts.lockPath, pid) };
|
|
1182
1402
|
} catch (err) {
|
|
1183
|
-
if (!(err instanceof Error) || !
|
|
1403
|
+
if (!(err instanceof Error) || !hasCode5(err, "EEXIST")) {
|
|
1184
1404
|
return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1185
1405
|
}
|
|
1186
1406
|
const existing = await inspectExisting(opts.lockPath, ttlMs);
|
|
@@ -1188,7 +1408,7 @@ async function acquireLock(opts) {
|
|
|
1188
1408
|
return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
|
|
1189
1409
|
}
|
|
1190
1410
|
try {
|
|
1191
|
-
await
|
|
1411
|
+
await unlink5(opts.lockPath);
|
|
1192
1412
|
} catch {
|
|
1193
1413
|
return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
|
|
1194
1414
|
}
|
|
@@ -1199,7 +1419,7 @@ async function acquireLock(opts) {
|
|
|
1199
1419
|
|
|
1200
1420
|
// src/lib/logger.ts
|
|
1201
1421
|
import { appendFile } from "fs/promises";
|
|
1202
|
-
import { tmpdir as
|
|
1422
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1203
1423
|
var DEFAULT_BUFFER_LIMIT = 4096;
|
|
1204
1424
|
var DEFAULT_FLUSH_INTERVAL_MS = 2e3;
|
|
1205
1425
|
function safeJson(data) {
|
|
@@ -1210,7 +1430,7 @@ function safeJson(data) {
|
|
|
1210
1430
|
}
|
|
1211
1431
|
}
|
|
1212
1432
|
function createLogger(opts = {}) {
|
|
1213
|
-
const filePath = opts.filePath ?? `${
|
|
1433
|
+
const filePath = opts.filePath ?? `${tmpdir4()}/opencoder-telegram.log`;
|
|
1214
1434
|
const namespace = opts.namespace ?? "default";
|
|
1215
1435
|
const bufferLimit = opts.bufferLimit ?? DEFAULT_BUFFER_LIMIT;
|
|
1216
1436
|
const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
|
|
@@ -1268,10 +1488,10 @@ function createLogger(opts = {}) {
|
|
|
1268
1488
|
}
|
|
1269
1489
|
|
|
1270
1490
|
// src/lib/state-store.ts
|
|
1271
|
-
import { mkdir as
|
|
1491
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
1272
1492
|
import { homedir as homedir2 } from "os";
|
|
1273
|
-
import { dirname as
|
|
1274
|
-
function
|
|
1493
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
1494
|
+
function hasCode6(err, code) {
|
|
1275
1495
|
return "code" in err && err.code === code;
|
|
1276
1496
|
}
|
|
1277
1497
|
function parseState(text) {
|
|
@@ -1283,28 +1503,28 @@ function parseState(text) {
|
|
|
1283
1503
|
return state;
|
|
1284
1504
|
}
|
|
1285
1505
|
function createStateStore(opts = {}) {
|
|
1286
|
-
const filePath = opts.filePath ??
|
|
1506
|
+
const filePath = opts.filePath ?? join6(homedir2(), ".config/opencode/telegram-remote/state.json");
|
|
1287
1507
|
return {
|
|
1288
1508
|
async read() {
|
|
1289
1509
|
try {
|
|
1290
|
-
return parseState(await
|
|
1510
|
+
return parseState(await readFile5(filePath, "utf8"));
|
|
1291
1511
|
} catch (err) {
|
|
1292
|
-
if (err instanceof Error &&
|
|
1512
|
+
if (err instanceof Error && hasCode6(err, "ENOENT")) return {};
|
|
1293
1513
|
throw err;
|
|
1294
1514
|
}
|
|
1295
1515
|
},
|
|
1296
1516
|
async write(patch) {
|
|
1297
1517
|
const existing = await this.read();
|
|
1298
1518
|
const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1299
|
-
await
|
|
1519
|
+
await mkdir5(dirname4(filePath), { recursive: true });
|
|
1300
1520
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1301
|
-
await
|
|
1521
|
+
await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1302
1522
|
try {
|
|
1303
|
-
await
|
|
1523
|
+
await rename4(tmpPath, filePath);
|
|
1304
1524
|
} catch (err) {
|
|
1305
|
-
if (!(err instanceof Error) || !
|
|
1306
|
-
await
|
|
1307
|
-
await
|
|
1525
|
+
if (!(err instanceof Error) || !hasCode6(err, "ENOENT")) throw err;
|
|
1526
|
+
await writeFile4(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1527
|
+
await rename4(tmpPath, filePath);
|
|
1308
1528
|
}
|
|
1309
1529
|
return next;
|
|
1310
1530
|
}
|
|
@@ -1312,6 +1532,10 @@ function createStateStore(opts = {}) {
|
|
|
1312
1532
|
}
|
|
1313
1533
|
|
|
1314
1534
|
// src/services/session-title-service.ts
|
|
1535
|
+
function agentFromSession(info) {
|
|
1536
|
+
const candidate = info;
|
|
1537
|
+
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
1538
|
+
}
|
|
1315
1539
|
var SessionTitleService = class {
|
|
1316
1540
|
sessions = /* @__PURE__ */ new Map();
|
|
1317
1541
|
setSessionInfo(info) {
|
|
@@ -1319,6 +1543,7 @@ var SessionTitleService = class {
|
|
|
1319
1543
|
this.sessions.set(info.id, {
|
|
1320
1544
|
title: info.title || null,
|
|
1321
1545
|
parentID: info.parentID ?? null,
|
|
1546
|
+
agent: agentFromSession(info) ?? existing?.agent,
|
|
1322
1547
|
status: existing?.status,
|
|
1323
1548
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1324
1549
|
});
|
|
@@ -1328,6 +1553,17 @@ var SessionTitleService = class {
|
|
|
1328
1553
|
this.sessions.set(sessionId, {
|
|
1329
1554
|
title,
|
|
1330
1555
|
parentID: existing?.parentID,
|
|
1556
|
+
agent: existing?.agent,
|
|
1557
|
+
status: existing?.status,
|
|
1558
|
+
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
setSessionAgent(sessionId, agent) {
|
|
1562
|
+
const existing = this.sessions.get(sessionId);
|
|
1563
|
+
this.sessions.set(sessionId, {
|
|
1564
|
+
title: existing?.title ?? null,
|
|
1565
|
+
parentID: existing?.parentID,
|
|
1566
|
+
agent,
|
|
1331
1567
|
status: existing?.status,
|
|
1332
1568
|
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
1333
1569
|
});
|
|
@@ -1337,6 +1573,7 @@ var SessionTitleService = class {
|
|
|
1337
1573
|
this.sessions.set(sessionId, {
|
|
1338
1574
|
title: existing?.title ?? null,
|
|
1339
1575
|
parentID: existing?.parentID,
|
|
1576
|
+
agent: existing?.agent,
|
|
1340
1577
|
status,
|
|
1341
1578
|
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
|
|
1342
1579
|
});
|
|
@@ -1347,6 +1584,9 @@ var SessionTitleService = class {
|
|
|
1347
1584
|
getParentID(sessionId) {
|
|
1348
1585
|
return this.sessions.get(sessionId)?.parentID;
|
|
1349
1586
|
}
|
|
1587
|
+
getSessionAgent(sessionId) {
|
|
1588
|
+
return this.sessions.get(sessionId)?.agent;
|
|
1589
|
+
}
|
|
1350
1590
|
getSessionStatus(sessionId) {
|
|
1351
1591
|
return this.sessions.get(sessionId)?.status;
|
|
1352
1592
|
}
|
|
@@ -1363,6 +1603,7 @@ var SessionTitleService = class {
|
|
|
1363
1603
|
this.sessions.set(sessionId, {
|
|
1364
1604
|
title: existing?.title ?? null,
|
|
1365
1605
|
parentID: existing?.parentID,
|
|
1606
|
+
agent: existing?.agent,
|
|
1366
1607
|
status: existing?.status ?? "idle",
|
|
1367
1608
|
idleNotificationPending: true
|
|
1368
1609
|
});
|
|
@@ -1381,7 +1622,7 @@ var SessionTitleService = class {
|
|
|
1381
1622
|
};
|
|
1382
1623
|
|
|
1383
1624
|
// src/telegram-remote.ts
|
|
1384
|
-
var pluginDir =
|
|
1625
|
+
var pluginDir = dirname5(fileURLToPath(import.meta.url));
|
|
1385
1626
|
async function postToServer(serverUrl, path, body) {
|
|
1386
1627
|
const url = new URL(path, serverUrl);
|
|
1387
1628
|
const response = await fetch(url, {
|
|
@@ -1391,7 +1632,22 @@ async function postToServer(serverUrl, path, body) {
|
|
|
1391
1632
|
});
|
|
1392
1633
|
if (response.ok) return;
|
|
1393
1634
|
const text = await response.text();
|
|
1394
|
-
throw new Error(
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
function getSessionAgentFromMessage(event) {
|
|
1640
|
+
const info = event.properties.info;
|
|
1641
|
+
if (info.role !== "user") return void 0;
|
|
1642
|
+
return { sessionID: info.sessionID, agent: info.agent };
|
|
1643
|
+
}
|
|
1644
|
+
function getSessionAgentFromNextStep(event) {
|
|
1645
|
+
if (event.type !== "session.next.step.started") return void 0;
|
|
1646
|
+
const props = event.properties;
|
|
1647
|
+
if (!props) return void 0;
|
|
1648
|
+
if (typeof props.sessionID !== "string") return void 0;
|
|
1649
|
+
if (typeof props.agent !== "string") return void 0;
|
|
1650
|
+
return { sessionID: props.sessionID, agent: props.agent };
|
|
1395
1651
|
}
|
|
1396
1652
|
var TelegramRemote = async (input) => {
|
|
1397
1653
|
const logger = createLogger({ namespace: "telegram" });
|
|
@@ -1401,11 +1657,12 @@ var TelegramRemote = async (input) => {
|
|
|
1401
1657
|
const config = loadConfig({ logger, env: process.env });
|
|
1402
1658
|
const stateStore = createStateStore();
|
|
1403
1659
|
const initialState = await stateStore.read();
|
|
1404
|
-
const tokenHash =
|
|
1405
|
-
const lockPath =
|
|
1406
|
-
const claimsDir =
|
|
1660
|
+
const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
|
|
1661
|
+
const lockPath = join7(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
|
|
1662
|
+
const claimsDir = join7(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
|
|
1407
1663
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1408
1664
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1665
|
+
const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
|
|
1409
1666
|
const lockResult = await acquireLock({ lockPath });
|
|
1410
1667
|
const isLeader = lockResult.acquired;
|
|
1411
1668
|
logger.info(
|
|
@@ -1459,6 +1716,19 @@ var TelegramRemote = async (input) => {
|
|
|
1459
1716
|
throwOnError: true
|
|
1460
1717
|
});
|
|
1461
1718
|
};
|
|
1719
|
+
const runSessionCommand = async (sessionID, command, serverUrl = input.serverUrl.href) => {
|
|
1720
|
+
const path = `/session/${encodeURIComponent(sessionID)}/command`;
|
|
1721
|
+
const body = { command, arguments: "" };
|
|
1722
|
+
if (serverUrl !== input.serverUrl.href) {
|
|
1723
|
+
await postToServer(serverUrl, path, body);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
await input.client.session.command({
|
|
1727
|
+
path: { id: sessionID },
|
|
1728
|
+
body,
|
|
1729
|
+
throwOnError: true
|
|
1730
|
+
});
|
|
1731
|
+
};
|
|
1462
1732
|
const bot = createTelegramBot({
|
|
1463
1733
|
config,
|
|
1464
1734
|
stateStore,
|
|
@@ -1503,12 +1773,15 @@ var TelegramRemote = async (input) => {
|
|
|
1503
1773
|
tokenHash,
|
|
1504
1774
|
pendingQuestions,
|
|
1505
1775
|
pendingPermissions,
|
|
1776
|
+
pendingStartWorks,
|
|
1506
1777
|
replyToQuestion,
|
|
1507
|
-
replyToPermission
|
|
1778
|
+
replyToPermission,
|
|
1779
|
+
runSessionCommand
|
|
1508
1780
|
};
|
|
1509
1781
|
if (isLeader) {
|
|
1510
1782
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1511
1783
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1784
|
+
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
1512
1785
|
}
|
|
1513
1786
|
return {
|
|
1514
1787
|
event: async ({ event }) => {
|
|
@@ -1523,9 +1796,20 @@ var TelegramRemote = async (input) => {
|
|
|
1523
1796
|
return handleSessionCreated(event, ctx);
|
|
1524
1797
|
case "session.updated":
|
|
1525
1798
|
return handleSessionUpdated(event, ctx);
|
|
1799
|
+
case "message.updated": {
|
|
1800
|
+
const messageAgent = getSessionAgentFromMessage(event);
|
|
1801
|
+
if (messageAgent)
|
|
1802
|
+
ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1526
1805
|
case "permission.updated":
|
|
1527
1806
|
return handlePermissionUpdated(event, ctx);
|
|
1528
1807
|
default: {
|
|
1808
|
+
const stepAgent = getSessionAgentFromNextStep(extEvent);
|
|
1809
|
+
if (stepAgent) {
|
|
1810
|
+
ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1529
1813
|
if (isEventPermissionAsked(extEvent)) {
|
|
1530
1814
|
if (!isLeader) return;
|
|
1531
1815
|
return handlePermissionAsked(extEvent, ctx);
|
|
@@ -1540,6 +1824,9 @@ var TelegramRemote = async (input) => {
|
|
|
1540
1824
|
if (isEventQuestionReplied(extEvent)) {
|
|
1541
1825
|
return handleQuestionReplied(extEvent, ctx);
|
|
1542
1826
|
}
|
|
1827
|
+
if (isEventPermissionReplied(extEvent)) {
|
|
1828
|
+
return handlePermissionReplied(extEvent, ctx);
|
|
1829
|
+
}
|
|
1543
1830
|
return;
|
|
1544
1831
|
}
|
|
1545
1832
|
}
|
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.12",
|
|
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",
|