@coinseeker/opencode-telegram-plugin 1.0.12 → 1.0.14
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 +4 -2
- package/dist/telegram-remote.js +1263 -62
- package/package.json +1 -1
package/dist/telegram-remote.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
// src/telegram-remote.ts
|
|
7
7
|
import { createHash as createHash5 } from "crypto";
|
|
8
|
-
import { tmpdir as tmpdir5 } from "os";
|
|
9
|
-
import { dirname as
|
|
8
|
+
import { homedir as homedir3, tmpdir as tmpdir5 } from "os";
|
|
9
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
|
|
12
12
|
// src/bot.ts
|
|
@@ -46,6 +46,11 @@ function createTelegramBot(opts) {
|
|
|
46
46
|
let questionDispatcher;
|
|
47
47
|
let permissionDispatcher;
|
|
48
48
|
let startWorkDispatcher;
|
|
49
|
+
let sessionsDispatcher;
|
|
50
|
+
let statusDispatcher;
|
|
51
|
+
let startWorkCommandDispatcher;
|
|
52
|
+
let helpDispatcher;
|
|
53
|
+
let managerObj;
|
|
49
54
|
if (polling) {
|
|
50
55
|
bot.use(async (ctx, next) => {
|
|
51
56
|
const userId = ctx.from?.id;
|
|
@@ -105,6 +110,36 @@ This chat is now active for OpenCode notifications.`
|
|
|
105
110
|
if (!startWorkDispatcher || messageId === void 0) return;
|
|
106
111
|
await startWorkDispatcher.handleCallbackQuery(data, messageId);
|
|
107
112
|
});
|
|
113
|
+
bot.command("sessions", async (ctx) => {
|
|
114
|
+
if (!sessionsDispatcher) return;
|
|
115
|
+
const chatId = ctx.chat?.id;
|
|
116
|
+
const userId = ctx.from?.id;
|
|
117
|
+
if (chatId === void 0 || userId === void 0) return;
|
|
118
|
+
await sessionsDispatcher({ chatId, userId, bot: managerObj });
|
|
119
|
+
});
|
|
120
|
+
bot.command("status", async (ctx) => {
|
|
121
|
+
if (!statusDispatcher) return;
|
|
122
|
+
const chatId = ctx.chat?.id;
|
|
123
|
+
const userId = ctx.from?.id;
|
|
124
|
+
if (chatId === void 0 || userId === void 0) return;
|
|
125
|
+
const args = ctx.match.trim().split(/\s+/).filter(Boolean);
|
|
126
|
+
await statusDispatcher({ chatId, userId, bot: managerObj, args });
|
|
127
|
+
});
|
|
128
|
+
bot.command("start_work", async (ctx) => {
|
|
129
|
+
if (!startWorkCommandDispatcher) return;
|
|
130
|
+
const chatId = ctx.chat?.id;
|
|
131
|
+
const userId = ctx.from?.id;
|
|
132
|
+
if (chatId === void 0 || userId === void 0) return;
|
|
133
|
+
const args = ctx.match.trim().split(/\s+/).filter(Boolean);
|
|
134
|
+
await startWorkCommandDispatcher({ chatId, userId, bot: managerObj, args });
|
|
135
|
+
});
|
|
136
|
+
bot.command("help", async (ctx) => {
|
|
137
|
+
if (!helpDispatcher) return;
|
|
138
|
+
const chatId = ctx.chat?.id;
|
|
139
|
+
const userId = ctx.from?.id;
|
|
140
|
+
if (chatId === void 0 || userId === void 0) return;
|
|
141
|
+
await helpDispatcher({ chatId, userId, bot: managerObj });
|
|
142
|
+
});
|
|
108
143
|
bot.on("message:text", async (ctx) => {
|
|
109
144
|
const replyToMessageId = ctx.message.reply_to_message?.message_id;
|
|
110
145
|
const chatId = ctx.chat.id;
|
|
@@ -122,12 +157,22 @@ This chat is now active for OpenCode notifications.`
|
|
|
122
157
|
}
|
|
123
158
|
throw new Error(`No active chat for ${action}. Send any message to the bot first.`);
|
|
124
159
|
};
|
|
125
|
-
|
|
160
|
+
managerObj = {
|
|
126
161
|
async start() {
|
|
127
162
|
if (!polling) {
|
|
128
163
|
logger.info("pass-through mode - skipping bot.start()");
|
|
129
164
|
return;
|
|
130
165
|
}
|
|
166
|
+
try {
|
|
167
|
+
await bot.api.setMyCommands([
|
|
168
|
+
{ command: "sessions", description: "\uD65C\uC131 \uC138\uC158 \uBAA9\uB85D (top 20)" },
|
|
169
|
+
{ command: "status", description: "\uC138\uC158 \uC0C1\uD0DC \uC870\uD68C (/status N)" },
|
|
170
|
+
{ command: "start_work", description: "plan-ready \uC138\uC158 \uC2E4\uD589 (/start_work N)" },
|
|
171
|
+
{ command: "help", description: "\uBA85\uB839 \uB3C4\uC6C0\uB9D0" }
|
|
172
|
+
]);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger.warn("setMyCommands failed", { error: String(err) });
|
|
175
|
+
}
|
|
131
176
|
await bot.start({
|
|
132
177
|
drop_pending_updates: true,
|
|
133
178
|
onStart: () => {
|
|
@@ -201,8 +246,21 @@ This chat is now active for OpenCode notifications.`
|
|
|
201
246
|
},
|
|
202
247
|
setStartWorkDispatcher(dispatcher) {
|
|
203
248
|
startWorkDispatcher = dispatcher;
|
|
249
|
+
},
|
|
250
|
+
setSessionsDispatcher(dispatcher) {
|
|
251
|
+
sessionsDispatcher = dispatcher;
|
|
252
|
+
},
|
|
253
|
+
setStatusDispatcher(dispatcher) {
|
|
254
|
+
statusDispatcher = dispatcher;
|
|
255
|
+
},
|
|
256
|
+
setStartWorkCommandDispatcher(dispatcher) {
|
|
257
|
+
startWorkCommandDispatcher = dispatcher;
|
|
258
|
+
},
|
|
259
|
+
setHelpDispatcher(dispatcher) {
|
|
260
|
+
helpDispatcher = dispatcher;
|
|
204
261
|
}
|
|
205
262
|
};
|
|
263
|
+
return managerObj;
|
|
206
264
|
}
|
|
207
265
|
|
|
208
266
|
// src/config.ts
|
|
@@ -959,9 +1017,313 @@ async function handleQuestionReplied(event, ctx) {
|
|
|
959
1017
|
}
|
|
960
1018
|
}
|
|
961
1019
|
|
|
1020
|
+
// src/lib/session-registry.ts
|
|
1021
|
+
import { chmod, mkdir as mkdir4, readFile as readFile3, readdir as readdir4, rename as rename3, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
1022
|
+
import { join as join4 } from "path";
|
|
1023
|
+
|
|
1024
|
+
// src/lib/opencode-http.ts
|
|
1025
|
+
var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
|
|
1026
|
+
function asRecord(value) {
|
|
1027
|
+
if (!value || typeof value !== "object") return void 0;
|
|
1028
|
+
return value;
|
|
1029
|
+
}
|
|
1030
|
+
function isStatusType(value) {
|
|
1031
|
+
return value === "idle" || value === "busy" || value === "retry";
|
|
1032
|
+
}
|
|
1033
|
+
function normalizeOpenCodeServerUrl(value) {
|
|
1034
|
+
if (!value) return void 0;
|
|
1035
|
+
try {
|
|
1036
|
+
const url = new URL(value);
|
|
1037
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
|
|
1038
|
+
if (url.username || url.password) return void 0;
|
|
1039
|
+
if (url.search || url.hash) return void 0;
|
|
1040
|
+
if (url.pathname !== "/" && url.pathname !== "") return void 0;
|
|
1041
|
+
if (!ALLOWED_HOSTNAMES.has(url.hostname)) return void 0;
|
|
1042
|
+
return url.href;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function requireOpenCodeServerUrl(serverUrl) {
|
|
1048
|
+
const normalized = normalizeOpenCodeServerUrl(serverUrl);
|
|
1049
|
+
if (!normalized) throw new Error("Invalid OpenCode server URL");
|
|
1050
|
+
return normalized;
|
|
1051
|
+
}
|
|
1052
|
+
function endpoint(serverUrl, path) {
|
|
1053
|
+
return new URL(path, requireOpenCodeServerUrl(serverUrl));
|
|
1054
|
+
}
|
|
1055
|
+
function isDifferentServerUrl(sourceServerUrl, currentServerUrl) {
|
|
1056
|
+
const source = normalizeOpenCodeServerUrl(sourceServerUrl);
|
|
1057
|
+
const current = normalizeOpenCodeServerUrl(currentServerUrl);
|
|
1058
|
+
if (!source || !current) return false;
|
|
1059
|
+
return source !== current;
|
|
1060
|
+
}
|
|
1061
|
+
function normalizeSession(value) {
|
|
1062
|
+
const record = asRecord(value);
|
|
1063
|
+
if (!record || typeof record.directory !== "string") return void 0;
|
|
1064
|
+
const session = { directory: record.directory };
|
|
1065
|
+
if (typeof record.id === "string") session.id = record.id;
|
|
1066
|
+
if (typeof record.title === "string") session.title = record.title;
|
|
1067
|
+
if (typeof record.parentID === "string" || record.parentID === null) {
|
|
1068
|
+
session.parentID = record.parentID;
|
|
1069
|
+
}
|
|
1070
|
+
if (typeof record.agent === "string") session.agent = record.agent;
|
|
1071
|
+
return session;
|
|
1072
|
+
}
|
|
1073
|
+
function normalizeSessionList(value) {
|
|
1074
|
+
if (!Array.isArray(value)) return [];
|
|
1075
|
+
const sessions = [];
|
|
1076
|
+
for (const raw of value) {
|
|
1077
|
+
const record = asRecord(raw);
|
|
1078
|
+
const time = asRecord(record?.time);
|
|
1079
|
+
if (!record) continue;
|
|
1080
|
+
if (typeof record.id !== "string") continue;
|
|
1081
|
+
if (typeof record.title !== "string") continue;
|
|
1082
|
+
if (!time || typeof time.updated !== "number") continue;
|
|
1083
|
+
const session = {
|
|
1084
|
+
id: record.id,
|
|
1085
|
+
title: record.title,
|
|
1086
|
+
time: { updated: time.updated }
|
|
1087
|
+
};
|
|
1088
|
+
if (typeof record.parentID === "string" || record.parentID === null) {
|
|
1089
|
+
session.parentID = record.parentID;
|
|
1090
|
+
}
|
|
1091
|
+
if (typeof record.agent === "string") session.agent = record.agent;
|
|
1092
|
+
sessions.push(session);
|
|
1093
|
+
}
|
|
1094
|
+
return sessions;
|
|
1095
|
+
}
|
|
1096
|
+
function normalizeStatusMap(value) {
|
|
1097
|
+
const record = asRecord(value);
|
|
1098
|
+
if (!record) return {};
|
|
1099
|
+
const out = {};
|
|
1100
|
+
for (const [sessionId, rawStatus] of Object.entries(record)) {
|
|
1101
|
+
const status = asRecord(rawStatus);
|
|
1102
|
+
if (status && isStatusType(status.type)) out[sessionId] = { type: status.type };
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
function normalizeMessages(value) {
|
|
1107
|
+
if (!Array.isArray(value)) return [];
|
|
1108
|
+
const messages = [];
|
|
1109
|
+
for (const rawMessage of value) {
|
|
1110
|
+
const message = asRecord(rawMessage);
|
|
1111
|
+
const info = asRecord(message?.info);
|
|
1112
|
+
if (!message || !info || typeof info.role !== "string" || !Array.isArray(message.parts)) continue;
|
|
1113
|
+
const parts = [];
|
|
1114
|
+
for (const rawPart of message.parts) {
|
|
1115
|
+
const part = asRecord(rawPart);
|
|
1116
|
+
if (!part || typeof part.type !== "string") continue;
|
|
1117
|
+
const normalized = { type: part.type };
|
|
1118
|
+
if (typeof part.text === "string") normalized.text = part.text;
|
|
1119
|
+
parts.push(normalized);
|
|
1120
|
+
}
|
|
1121
|
+
messages.push({ info: { role: info.role }, parts });
|
|
1122
|
+
}
|
|
1123
|
+
return messages;
|
|
1124
|
+
}
|
|
1125
|
+
async function fetchJson(serverUrl, path, fetcher) {
|
|
1126
|
+
const response = await fetcher(endpoint(serverUrl, path), { redirect: "error" });
|
|
1127
|
+
if (response.status === 404) return { data: void 0, response: { status: response.status } };
|
|
1128
|
+
if (!response.ok) {
|
|
1129
|
+
throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
|
|
1130
|
+
}
|
|
1131
|
+
return { data: await response.json(), response: { status: response.status } };
|
|
1132
|
+
}
|
|
1133
|
+
async function getRemoteSession(serverUrl, sessionId, fetcher = fetch) {
|
|
1134
|
+
const result = await fetchJson(serverUrl, `/session/${encodeURIComponent(sessionId)}`, fetcher);
|
|
1135
|
+
return { data: normalizeSession(result.data), response: result.response };
|
|
1136
|
+
}
|
|
1137
|
+
async function getRemoteStatusMap(serverUrl, fetcher = fetch) {
|
|
1138
|
+
const result = await fetchJson(serverUrl, "/session/status", fetcher);
|
|
1139
|
+
return normalizeStatusMap(result.data);
|
|
1140
|
+
}
|
|
1141
|
+
async function getRemoteSessions(serverUrl, fetcher = fetch) {
|
|
1142
|
+
const result = await fetchJson(serverUrl, "/session", fetcher);
|
|
1143
|
+
return normalizeSessionList(result.data);
|
|
1144
|
+
}
|
|
1145
|
+
async function getRemoteMessages(serverUrl, sessionId, limit, fetcher = fetch) {
|
|
1146
|
+
const url = endpoint(serverUrl, `/session/${encodeURIComponent(sessionId)}/message`);
|
|
1147
|
+
url.searchParams.set("limit", String(limit));
|
|
1148
|
+
const response = await fetcher(url, { redirect: "error" });
|
|
1149
|
+
if (response.status === 404) return [];
|
|
1150
|
+
if (!response.ok) {
|
|
1151
|
+
throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
|
|
1152
|
+
}
|
|
1153
|
+
return normalizeMessages(await response.json());
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/lib/session-registry.ts
|
|
1157
|
+
function filenameForSession(sessionId) {
|
|
1158
|
+
return Buffer.from(sessionId).toString("base64url") + ".json";
|
|
1159
|
+
}
|
|
1160
|
+
function hasCode4(err, code) {
|
|
1161
|
+
return err instanceof Error && "code" in err && err.code === code;
|
|
1162
|
+
}
|
|
1163
|
+
function normalizeEntry(entry) {
|
|
1164
|
+
const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
|
|
1165
|
+
if (!serverUrl) throw new Error("invalid registry serverUrl");
|
|
1166
|
+
const out = {
|
|
1167
|
+
sessionId: entry.sessionId,
|
|
1168
|
+
title: entry.title,
|
|
1169
|
+
parentID: entry.parentID,
|
|
1170
|
+
serverUrl,
|
|
1171
|
+
updatedAt: entry.updatedAt
|
|
1172
|
+
};
|
|
1173
|
+
if (entry.agent !== void 0) out.agent = entry.agent;
|
|
1174
|
+
if (entry.status !== void 0) out.status = entry.status;
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
function isRegistryFile(value) {
|
|
1178
|
+
if (!value || typeof value !== "object") return false;
|
|
1179
|
+
const file = value;
|
|
1180
|
+
if (file.version !== 1) return false;
|
|
1181
|
+
const entry = file.entry;
|
|
1182
|
+
if (!entry || typeof entry !== "object") return false;
|
|
1183
|
+
const e = entry;
|
|
1184
|
+
if (typeof e.sessionId !== "string") return false;
|
|
1185
|
+
if (typeof e.title !== "string") return false;
|
|
1186
|
+
if (e.parentID !== null && typeof e.parentID !== "string") return false;
|
|
1187
|
+
if (e.agent !== void 0 && typeof e.agent !== "string") return false;
|
|
1188
|
+
if (e.status !== void 0 && e.status !== "idle" && e.status !== "busy" && e.status !== "retry") {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof e.serverUrl !== "string") return false;
|
|
1192
|
+
if (!normalizeOpenCodeServerUrl(e.serverUrl)) return false;
|
|
1193
|
+
if (typeof e.updatedAt !== "number") return false;
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
function agentFromSession(session) {
|
|
1197
|
+
const candidate = session;
|
|
1198
|
+
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
1199
|
+
}
|
|
1200
|
+
function registryEntryFromSession(session, serverUrl, status) {
|
|
1201
|
+
const entry = {
|
|
1202
|
+
sessionId: session.id,
|
|
1203
|
+
title: session.title,
|
|
1204
|
+
parentID: session.parentID ?? null,
|
|
1205
|
+
serverUrl,
|
|
1206
|
+
updatedAt: session.time.updated
|
|
1207
|
+
};
|
|
1208
|
+
const agent = agentFromSession(session);
|
|
1209
|
+
if (agent !== void 0) entry.agent = agent;
|
|
1210
|
+
if (status !== void 0) entry.status = status;
|
|
1211
|
+
return entry;
|
|
1212
|
+
}
|
|
1213
|
+
function createSessionRegistryStore(opts) {
|
|
1214
|
+
const registryDir = join4(opts.configDir, "session-registry", opts.tokenHash);
|
|
1215
|
+
function filePath(sessionId) {
|
|
1216
|
+
return join4(registryDir, filenameForSession(sessionId));
|
|
1217
|
+
}
|
|
1218
|
+
async function readEntry(sessionId) {
|
|
1219
|
+
let text;
|
|
1220
|
+
try {
|
|
1221
|
+
text = await readFile3(filePath(sessionId), "utf8");
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
if (hasCode4(err, "ENOENT")) return null;
|
|
1224
|
+
opts.logger.error("session-registry: failed to read file", {
|
|
1225
|
+
sessionId,
|
|
1226
|
+
error: String(err)
|
|
1227
|
+
});
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
const parsed = JSON.parse(text);
|
|
1232
|
+
if (!isRegistryFile(parsed)) return null;
|
|
1233
|
+
return normalizeEntry(parsed.entry);
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
opts.logger.error("session-registry: corrupted JSON", { sessionId, error: String(err) });
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async function writeEntry(entry) {
|
|
1240
|
+
await mkdir4(registryDir, { recursive: true });
|
|
1241
|
+
try {
|
|
1242
|
+
await chmod(registryDir, 448);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
opts.logger.error("session-registry: failed to chmod dir", { error: String(err) });
|
|
1245
|
+
}
|
|
1246
|
+
const payload = { version: 1, entry: normalizeEntry(entry) };
|
|
1247
|
+
const target = filePath(entry.sessionId);
|
|
1248
|
+
const tmp = `${target}.tmp.${process.pid}.${Date.now()}`;
|
|
1249
|
+
await writeFile3(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
1250
|
+
try {
|
|
1251
|
+
await rename3(tmp, target);
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
try {
|
|
1254
|
+
await unlink4(tmp);
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
throw err;
|
|
1258
|
+
}
|
|
1259
|
+
await chmod(target, 384);
|
|
1260
|
+
}
|
|
1261
|
+
async function upsertSession(entry) {
|
|
1262
|
+
const existing = await readEntry(entry.sessionId);
|
|
1263
|
+
await writeEntry({
|
|
1264
|
+
...existing,
|
|
1265
|
+
...entry,
|
|
1266
|
+
agent: entry.agent ?? existing?.agent,
|
|
1267
|
+
status: entry.status ?? existing?.status
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
async function updateSession(sessionId, patch) {
|
|
1271
|
+
const existing = await readEntry(sessionId);
|
|
1272
|
+
if (!existing) return;
|
|
1273
|
+
await writeEntry({
|
|
1274
|
+
...existing,
|
|
1275
|
+
...patch,
|
|
1276
|
+
sessionId,
|
|
1277
|
+
title: patch.title ?? existing.title,
|
|
1278
|
+
parentID: patch.parentID ?? existing.parentID,
|
|
1279
|
+
serverUrl: patch.serverUrl ?? existing.serverUrl,
|
|
1280
|
+
updatedAt: patch.updatedAt ?? Date.now()
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
async function listSessions() {
|
|
1284
|
+
let names;
|
|
1285
|
+
try {
|
|
1286
|
+
names = await readdir4(registryDir);
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
if (hasCode4(err, "ENOENT")) return [];
|
|
1289
|
+
opts.logger.error("session-registry: failed to list dir", { error: String(err) });
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
const entries = [];
|
|
1293
|
+
for (const name of names) {
|
|
1294
|
+
if (!name.endsWith(".json")) continue;
|
|
1295
|
+
let text;
|
|
1296
|
+
try {
|
|
1297
|
+
text = await readFile3(join4(registryDir, name), "utf8");
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
opts.logger.error("session-registry: failed to read listed file", {
|
|
1300
|
+
file: name,
|
|
1301
|
+
error: String(err)
|
|
1302
|
+
});
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
try {
|
|
1306
|
+
const parsed = JSON.parse(text);
|
|
1307
|
+
if (isRegistryFile(parsed)) entries.push(normalizeEntry(parsed.entry));
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
opts.logger.error("session-registry: corrupted listed file", {
|
|
1310
|
+
file: name,
|
|
1311
|
+
error: String(err)
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return entries;
|
|
1316
|
+
}
|
|
1317
|
+
return { upsertSession, updateSession, listSessions };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
962
1320
|
// src/events/session-created.ts
|
|
963
1321
|
async function handleSessionCreated(event, ctx) {
|
|
964
|
-
|
|
1322
|
+
const info = event.properties.info;
|
|
1323
|
+
ctx.sessionTitleService.setSessionInfo(info);
|
|
1324
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1325
|
+
registryEntryFromSession(info, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(info.id))
|
|
1326
|
+
);
|
|
965
1327
|
}
|
|
966
1328
|
|
|
967
1329
|
// src/lib/abort-tracker.ts
|
|
@@ -1003,14 +1365,14 @@ async function handleSessionError(event, ctx) {
|
|
|
1003
1365
|
|
|
1004
1366
|
// src/lib/pending-start-work.ts
|
|
1005
1367
|
import { createHash as createHash4 } from "crypto";
|
|
1006
|
-
import { mkdir as
|
|
1368
|
+
import { mkdir as mkdir5, readdir as readdir5, readFile as readFile4, rename as rename4, unlink as unlink5, writeFile as writeFile4 } from "fs/promises";
|
|
1007
1369
|
import { tmpdir as tmpdir3 } from "os";
|
|
1008
|
-
import { dirname as dirname3, join as
|
|
1009
|
-
function
|
|
1370
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1371
|
+
function hasCode5(err, code) {
|
|
1010
1372
|
return "code" in err && err.code === code;
|
|
1011
1373
|
}
|
|
1012
1374
|
function pendingFilePath3(dir, shortHash) {
|
|
1013
|
-
return
|
|
1375
|
+
return join5(dir, `${shortHash}.json`);
|
|
1014
1376
|
}
|
|
1015
1377
|
function parsePending3(text) {
|
|
1016
1378
|
const parsed = JSON.parse(text);
|
|
@@ -1029,10 +1391,10 @@ function parsePending3(text) {
|
|
|
1029
1391
|
}
|
|
1030
1392
|
async function listPendingFiles3(dir) {
|
|
1031
1393
|
try {
|
|
1032
|
-
const entries = await
|
|
1394
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
1033
1395
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
|
|
1034
1396
|
} catch (err) {
|
|
1035
|
-
if (err instanceof Error &&
|
|
1397
|
+
if (err instanceof Error && hasCode5(err, "ENOENT")) return [];
|
|
1036
1398
|
throw err;
|
|
1037
1399
|
}
|
|
1038
1400
|
}
|
|
@@ -1040,29 +1402,29 @@ function shortHashFromFileName3(fileName) {
|
|
|
1040
1402
|
return fileName.slice(0, -".json".length);
|
|
1041
1403
|
}
|
|
1042
1404
|
function createPendingStartWorkStore(opts) {
|
|
1043
|
-
const dir = opts.baseDir ??
|
|
1405
|
+
const dir = opts.baseDir ?? join5(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
|
|
1044
1406
|
return {
|
|
1045
1407
|
dir,
|
|
1046
1408
|
async savePending(shortHash, data) {
|
|
1047
1409
|
const filePath = pendingFilePath3(dir, shortHash);
|
|
1048
|
-
await
|
|
1410
|
+
await mkdir5(dirname3(filePath), { recursive: true });
|
|
1049
1411
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1050
|
-
await
|
|
1051
|
-
await
|
|
1412
|
+
await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
1413
|
+
await rename4(tmpPath, filePath);
|
|
1052
1414
|
},
|
|
1053
1415
|
async loadPending(shortHash) {
|
|
1054
1416
|
try {
|
|
1055
|
-
return parsePending3(await
|
|
1417
|
+
return parsePending3(await readFile4(pendingFilePath3(dir, shortHash), "utf8"));
|
|
1056
1418
|
} catch (err) {
|
|
1057
|
-
if (err instanceof Error &&
|
|
1419
|
+
if (err instanceof Error && hasCode5(err, "ENOENT")) return void 0;
|
|
1058
1420
|
throw err;
|
|
1059
1421
|
}
|
|
1060
1422
|
},
|
|
1061
1423
|
async deletePending(shortHash) {
|
|
1062
1424
|
try {
|
|
1063
|
-
await
|
|
1425
|
+
await unlink5(pendingFilePath3(dir, shortHash));
|
|
1064
1426
|
} catch (err) {
|
|
1065
|
-
if (!(err instanceof Error) || !
|
|
1427
|
+
if (!(err instanceof Error) || !hasCode5(err, "ENOENT")) throw err;
|
|
1066
1428
|
}
|
|
1067
1429
|
},
|
|
1068
1430
|
async sweepExpired() {
|
|
@@ -1170,6 +1532,13 @@ async function resolveParentID(sessionId, ctx) {
|
|
|
1170
1532
|
const result = await ctx.client.session.get({ path: { id: sessionId } });
|
|
1171
1533
|
if (result.data) {
|
|
1172
1534
|
ctx.sessionTitleService.setSessionInfo(result.data);
|
|
1535
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1536
|
+
registryEntryFromSession(
|
|
1537
|
+
result.data,
|
|
1538
|
+
ctx.serverUrl.href,
|
|
1539
|
+
ctx.sessionTitleService.getSessionStatus(sessionId)
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1173
1542
|
return ctx.sessionTitleService.getParentID(sessionId);
|
|
1174
1543
|
}
|
|
1175
1544
|
ctx.logger.warn("session parentID cache miss fetch returned no data", { sessionId });
|
|
@@ -1186,6 +1555,9 @@ async function hydrateDescendants(sessionId, ctx, seen = /* @__PURE__ */ new Set
|
|
|
1186
1555
|
const result = await ctx.client.session.children({ path: { id: sessionId } });
|
|
1187
1556
|
for (const child of result.data ?? []) {
|
|
1188
1557
|
ctx.sessionTitleService.setSessionInfo(child);
|
|
1558
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1559
|
+
registryEntryFromSession(child, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(child.id))
|
|
1560
|
+
);
|
|
1189
1561
|
await hydrateDescendants(child.id, ctx, seen);
|
|
1190
1562
|
}
|
|
1191
1563
|
} catch (err) {
|
|
@@ -1255,6 +1627,7 @@ async function handleSessionIdle(event, ctx) {
|
|
|
1255
1627
|
const sessionId = event.properties.sessionID;
|
|
1256
1628
|
const parentID = await resolveParentID(sessionId, ctx);
|
|
1257
1629
|
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1630
|
+
await ctx.sessionRegistry.updateSession(sessionId, { status: "idle", updatedAt: Date.now() });
|
|
1258
1631
|
if (typeof parentID === "string") {
|
|
1259
1632
|
ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
|
|
1260
1633
|
await flushDeferredParentIfReady(parentID, ctx);
|
|
@@ -1281,9 +1654,14 @@ async function handleSessionIdle(event, ctx) {
|
|
|
1281
1654
|
async function handleSessionStatus(event, ctx) {
|
|
1282
1655
|
const sessionId = event.properties.sessionID;
|
|
1283
1656
|
const statusType = event.properties.status.type;
|
|
1657
|
+
const previousStatus = ctx.sessionTitleService.getSessionStatus(sessionId);
|
|
1284
1658
|
ctx.sessionTitleService.setSessionStatus(sessionId, statusType);
|
|
1285
1659
|
if (statusType === "idle") {
|
|
1286
1660
|
await handleSessionIdle(event, ctx);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (previousStatus !== statusType) {
|
|
1664
|
+
await ctx.sessionRegistry.updateSession(sessionId, { status: statusType, updatedAt: Date.now() });
|
|
1287
1665
|
}
|
|
1288
1666
|
}
|
|
1289
1667
|
|
|
@@ -1291,19 +1669,605 @@ async function handleSessionStatus(event, ctx) {
|
|
|
1291
1669
|
async function handleSessionUpdated(event, ctx) {
|
|
1292
1670
|
const info = event.properties.info;
|
|
1293
1671
|
ctx.sessionTitleService.setSessionInfo(info);
|
|
1672
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1673
|
+
registryEntryFromSession(info, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(info.id))
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/lib/html-escape.ts
|
|
1678
|
+
function escapeHtml(input) {
|
|
1679
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1680
|
+
}
|
|
1681
|
+
function truncateForTelegram(input, maxChars, ellipsis = "\u2026") {
|
|
1682
|
+
const single = input.replace(/\s+/g, " ").trim();
|
|
1683
|
+
if (single.length <= maxChars) return single;
|
|
1684
|
+
if (maxChars <= 0) return "";
|
|
1685
|
+
if (ellipsis.length >= maxChars) return ellipsis.slice(0, maxChars);
|
|
1686
|
+
return single.slice(0, maxChars - ellipsis.length) + ellipsis;
|
|
1687
|
+
}
|
|
1688
|
+
function stripCodeFences(input) {
|
|
1689
|
+
return input.replace(/```[^\r\n`]*\r?\n([\s\S]*?)```/g, "$1").replace(/```([\s\S]*?)```/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/\s+/g, " ").trim();
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/events/sessions-command.ts
|
|
1693
|
+
var MAX_BODY_CHARS = 3900;
|
|
1694
|
+
var MAX_TITLE_CHARS = 55;
|
|
1695
|
+
var MAX_SESSIONS = 20;
|
|
1696
|
+
function agentFromSession2(session) {
|
|
1697
|
+
const candidate = session;
|
|
1698
|
+
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
function isRootSession(session) {
|
|
1701
|
+
return session.parentID === void 0 || session.parentID === null;
|
|
1702
|
+
}
|
|
1703
|
+
function isRootRegistryEntry(entry) {
|
|
1704
|
+
return entry.parentID === null;
|
|
1705
|
+
}
|
|
1706
|
+
function isRootRemoteSession(session) {
|
|
1707
|
+
return session.parentID === void 0 || session.parentID === null;
|
|
1708
|
+
}
|
|
1709
|
+
function addRegistryRecord(combined, entry, status) {
|
|
1710
|
+
combined.set(entry.sessionId, {
|
|
1711
|
+
sessionId: entry.sessionId,
|
|
1712
|
+
title: entry.title,
|
|
1713
|
+
agent: entry.agent,
|
|
1714
|
+
status: status ?? entry.status ?? "idle",
|
|
1715
|
+
serverUrl: entry.serverUrl,
|
|
1716
|
+
updatedAt: entry.updatedAt
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
async function addRemoteServerRecords(combined, serverUrl, deps) {
|
|
1720
|
+
const [remoteSessions, remoteStatusMap] = await Promise.all([
|
|
1721
|
+
getRemoteSessions(serverUrl, deps.opencodeFetch),
|
|
1722
|
+
getRemoteStatusMap(serverUrl, deps.opencodeFetch)
|
|
1723
|
+
]);
|
|
1724
|
+
for (const session of remoteSessions.filter(isRootRemoteSession)) {
|
|
1725
|
+
const status = remoteStatusMap[session.id]?.type ?? "idle";
|
|
1726
|
+
combined.set(session.id, {
|
|
1727
|
+
sessionId: session.id,
|
|
1728
|
+
title: session.title,
|
|
1729
|
+
agent: session.agent,
|
|
1730
|
+
status,
|
|
1731
|
+
serverUrl,
|
|
1732
|
+
updatedAt: session.time.updated
|
|
1733
|
+
});
|
|
1734
|
+
await deps.sessionRegistry.upsertSession({
|
|
1735
|
+
sessionId: session.id,
|
|
1736
|
+
title: session.title,
|
|
1737
|
+
parentID: session.parentID ?? null,
|
|
1738
|
+
agent: session.agent,
|
|
1739
|
+
status,
|
|
1740
|
+
serverUrl,
|
|
1741
|
+
updatedAt: session.time.updated
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function createSessionsDispatcher(deps) {
|
|
1746
|
+
return async ({ chatId, bot }) => {
|
|
1747
|
+
let sessions;
|
|
1748
|
+
try {
|
|
1749
|
+
const [listResult, statusResult] = await Promise.all([
|
|
1750
|
+
deps.client.session.list(),
|
|
1751
|
+
deps.client.session.status()
|
|
1752
|
+
]);
|
|
1753
|
+
const statusMap = statusResult.data ?? {};
|
|
1754
|
+
for (const session of listResult.data ?? []) {
|
|
1755
|
+
deps.sessionTitleService.setSessionInfo(session);
|
|
1756
|
+
deps.sessionTitleService.setServerUrl(session.id, deps.serverUrl);
|
|
1757
|
+
const status = statusMap[session.id]?.type ?? "idle";
|
|
1758
|
+
await deps.sessionRegistry.upsertSession(
|
|
1759
|
+
registryEntryFromSession(session, deps.serverUrl, status)
|
|
1760
|
+
);
|
|
1761
|
+
if (status !== void 0) deps.sessionTitleService.setSessionStatus(session.id, status);
|
|
1762
|
+
}
|
|
1763
|
+
const registrySessions = await deps.sessionRegistry.listSessions();
|
|
1764
|
+
const combined = /* @__PURE__ */ new Map();
|
|
1765
|
+
const remoteServerUrls = /* @__PURE__ */ new Set();
|
|
1766
|
+
for (const entry of registrySessions.filter(isRootRegistryEntry)) {
|
|
1767
|
+
const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
|
|
1768
|
+
if (!serverUrl) continue;
|
|
1769
|
+
if (isDifferentServerUrl(serverUrl, deps.serverUrl)) {
|
|
1770
|
+
remoteServerUrls.add(serverUrl);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
addRegistryRecord(combined, { ...entry, serverUrl }, statusMap[entry.sessionId]?.type);
|
|
1774
|
+
}
|
|
1775
|
+
for (const serverUrl of remoteServerUrls) {
|
|
1776
|
+
try {
|
|
1777
|
+
await addRemoteServerRecords(combined, serverUrl, deps);
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
deps.logger.error("sessions remote server refresh failed", { serverUrl, error: String(err) });
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
for (const session of (listResult.data ?? []).filter(isRootSession)) {
|
|
1783
|
+
combined.set(session.id, {
|
|
1784
|
+
sessionId: session.id,
|
|
1785
|
+
title: session.title,
|
|
1786
|
+
agent: agentFromSession2(session),
|
|
1787
|
+
status: statusMap[session.id]?.type ?? "idle",
|
|
1788
|
+
serverUrl: deps.serverUrl,
|
|
1789
|
+
updatedAt: session.time.updated
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
sessions = [...combined.values()].sort((a, b) => b.updatedAt - a.updatedAt).slice(0, MAX_SESSIONS);
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
await bot.sendMessage("\uC138\uC158 \uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
|
|
1795
|
+
deps.logger.error("sessions list failed", { chatId, error: String(err) });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (sessions.length === 0) {
|
|
1799
|
+
await bot.sendMessage("\uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const capturedAt = Date.now();
|
|
1803
|
+
const entries = sessions.map((session, i) => {
|
|
1804
|
+
const entry = {
|
|
1805
|
+
index: i + 1,
|
|
1806
|
+
sessionId: session.sessionId,
|
|
1807
|
+
title: session.title,
|
|
1808
|
+
capturedAt
|
|
1809
|
+
};
|
|
1810
|
+
if (session.agent !== void 0) entry.agent = session.agent;
|
|
1811
|
+
entry.status = session.status;
|
|
1812
|
+
if (session.serverUrl !== void 0) entry.serverUrl = session.serverUrl;
|
|
1813
|
+
return entry;
|
|
1814
|
+
});
|
|
1815
|
+
await deps.snapshotStore.saveSnapshot(chatId, entries);
|
|
1816
|
+
const lines = entries.map((entry) => {
|
|
1817
|
+
const agent = entry.agent ? escapeHtml(entry.agent) : "?";
|
|
1818
|
+
const title = truncateForTelegram(escapeHtml(entry.title), MAX_TITLE_CHARS);
|
|
1819
|
+
const status = ` \u2014 ${escapeHtml(entry.status ?? "idle")}`;
|
|
1820
|
+
return `${entry.index}. [${agent}] ${title}${status}`;
|
|
1821
|
+
});
|
|
1822
|
+
let body = lines.join("\n");
|
|
1823
|
+
if (body.length > MAX_BODY_CHARS) {
|
|
1824
|
+
body = body.slice(0, MAX_BODY_CHARS) + "\u2026";
|
|
1825
|
+
}
|
|
1826
|
+
const text = `<b>\uCD5C\uADFC \uC138\uC158 (top ${entries.length})</b>
|
|
1827
|
+
${body}
|
|
1828
|
+
|
|
1829
|
+
<i>/status N \uB610\uB294 /start_work N \uC73C\uB85C \uC870\uC791</i>`;
|
|
1830
|
+
await bot.sendMessage(text, { parse_mode: "HTML" });
|
|
1831
|
+
deps.logger.info("sessions listed", { chatId, count: entries.length });
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// src/lib/plan-readiness.ts
|
|
1836
|
+
import { access, readFile as readFile5, readdir as readdir6, stat as stat2 } from "fs/promises";
|
|
1837
|
+
import { join as join6 } from "path";
|
|
1838
|
+
async function checkPlanReadiness(args) {
|
|
1839
|
+
const { projectRoot } = args;
|
|
1840
|
+
const omoDir = join6(projectRoot, ".omo");
|
|
1841
|
+
const plansDir = join6(omoDir, "plans");
|
|
1842
|
+
const boulderPath = join6(omoDir, "boulder.json");
|
|
1843
|
+
try {
|
|
1844
|
+
await access(omoDir);
|
|
1845
|
+
} catch {
|
|
1846
|
+
return {
|
|
1847
|
+
ready: false,
|
|
1848
|
+
reason: "no-omo-dir",
|
|
1849
|
+
detail: `${omoDir} does not exist`
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
try {
|
|
1853
|
+
await access(boulderPath);
|
|
1854
|
+
return {
|
|
1855
|
+
ready: false,
|
|
1856
|
+
reason: "boulder-active",
|
|
1857
|
+
detail: `${boulderPath} exists`
|
|
1858
|
+
};
|
|
1859
|
+
} catch {
|
|
1860
|
+
}
|
|
1861
|
+
let planFiles = [];
|
|
1862
|
+
try {
|
|
1863
|
+
const entries = await readdir6(plansDir);
|
|
1864
|
+
planFiles = entries.filter((e) => e.endsWith(".md"));
|
|
1865
|
+
} catch {
|
|
1866
|
+
return {
|
|
1867
|
+
ready: false,
|
|
1868
|
+
reason: "no-plans",
|
|
1869
|
+
detail: `${plansDir} not found or empty`
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
if (planFiles.length === 0) {
|
|
1873
|
+
return {
|
|
1874
|
+
ready: false,
|
|
1875
|
+
reason: "no-plans",
|
|
1876
|
+
detail: `No .md files in ${plansDir}`
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
const stats = await Promise.all(
|
|
1880
|
+
planFiles.map(async (f) => {
|
|
1881
|
+
const full = join6(plansDir, f);
|
|
1882
|
+
const s = await stat2(full);
|
|
1883
|
+
return { path: full, name: f, mtime: s.mtime.getTime() };
|
|
1884
|
+
})
|
|
1885
|
+
);
|
|
1886
|
+
stats.sort((a, b) => b.mtime - a.mtime);
|
|
1887
|
+
const latest = stats[0];
|
|
1888
|
+
const content = await readFile5(latest.path, "utf8");
|
|
1889
|
+
const totalMatches = content.match(/^- \[[ xX]\]/gm) ?? [];
|
|
1890
|
+
const completedMatches = content.match(/^- \[[xX]\]/gm) ?? [];
|
|
1891
|
+
const total = totalMatches.length;
|
|
1892
|
+
const completed = completedMatches.length;
|
|
1893
|
+
if (total === 0) {
|
|
1894
|
+
return {
|
|
1895
|
+
ready: false,
|
|
1896
|
+
reason: "plan-empty",
|
|
1897
|
+
detail: `${latest.name}: no checkboxes found`
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
if (completed >= total) {
|
|
1901
|
+
return {
|
|
1902
|
+
ready: false,
|
|
1903
|
+
reason: "all-plans-complete",
|
|
1904
|
+
detail: `${latest.name}: ${completed}/${total} complete`
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
ready: true,
|
|
1909
|
+
planPath: latest.path,
|
|
1910
|
+
planName: latest.name.replace(/\.md$/, ""),
|
|
1911
|
+
total,
|
|
1912
|
+
completed
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
async function recheckSessionIdle(client, sessionId) {
|
|
1916
|
+
const result = await client.session.status();
|
|
1917
|
+
const statuses = result.data ?? {};
|
|
1918
|
+
const sessionStatus = statuses[sessionId];
|
|
1919
|
+
return (sessionStatus?.type ?? "idle") === "idle";
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// src/events/status-command.ts
|
|
1923
|
+
var SNIPPET_MAX_CHARS = 80;
|
|
1924
|
+
var MESSAGES_LIMIT = 10;
|
|
1925
|
+
var EMPTY_MESSAGE = "\uBA54\uC2DC\uC9C0 \uC5C6\uC74C";
|
|
1926
|
+
function resolveProjectRoot(session) {
|
|
1927
|
+
if (!session.directory) throw new Error("session directory missing");
|
|
1928
|
+
return session.directory;
|
|
1929
|
+
}
|
|
1930
|
+
function extractTextFromParts(parts) {
|
|
1931
|
+
const pieces = [];
|
|
1932
|
+
for (const part of parts) {
|
|
1933
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1934
|
+
pieces.push(part.text);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return pieces.join(" ");
|
|
1938
|
+
}
|
|
1939
|
+
function buildSnippet(envelope) {
|
|
1940
|
+
if (!envelope) return EMPTY_MESSAGE;
|
|
1941
|
+
try {
|
|
1942
|
+
const raw = extractTextFromParts(envelope.parts);
|
|
1943
|
+
const cleaned = stripCodeFences(raw);
|
|
1944
|
+
const truncated = truncateForTelegram(cleaned, SNIPPET_MAX_CHARS);
|
|
1945
|
+
if (!truncated) return EMPTY_MESSAGE;
|
|
1946
|
+
return escapeHtml(truncated);
|
|
1947
|
+
} catch {
|
|
1948
|
+
return EMPTY_MESSAGE;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function findLastByRole(messages, role) {
|
|
1952
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1953
|
+
const msg = messages[i];
|
|
1954
|
+
if (msg && msg.info.role === role) return msg;
|
|
1955
|
+
}
|
|
1956
|
+
return void 0;
|
|
1957
|
+
}
|
|
1958
|
+
function planReadinessKorean(result) {
|
|
1959
|
+
if (result.ready) {
|
|
1960
|
+
return `${result.completed}/${result.total} (${result.planName})`;
|
|
1961
|
+
}
|
|
1962
|
+
switch (result.reason) {
|
|
1963
|
+
case "no-omo-dir":
|
|
1964
|
+
return "`.omo/` \uC5C6\uC74C";
|
|
1965
|
+
case "no-plans":
|
|
1966
|
+
return "plan \uD30C\uC77C \uC5C6\uC74C";
|
|
1967
|
+
case "plan-empty":
|
|
1968
|
+
return "\uCCB4\uD06C\uBC15\uC2A4 \uC5C6\uC74C";
|
|
1969
|
+
case "all-plans-complete": {
|
|
1970
|
+
const match = result.detail.match(/(\d+)\/(\d+)/);
|
|
1971
|
+
if (match) return `${match[1]}/${match[2]} \uC644\uB8CC`;
|
|
1972
|
+
return "\uC644\uB8CC";
|
|
1973
|
+
}
|
|
1974
|
+
case "boulder-active":
|
|
1975
|
+
return "boulder \uD65C\uC131";
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
function planLine(result) {
|
|
1979
|
+
if (result.ready) {
|
|
1980
|
+
return `<b>\uD50C\uB79C \uC9C4\uD589\uB3C4</b>: ${result.completed}/${result.total} (${escapeHtml(result.planName)})`;
|
|
1981
|
+
}
|
|
1982
|
+
return `<b>\uD50C\uB79C \uC0C1\uD0DC</b>: ${planReadinessKorean(result)}`;
|
|
1983
|
+
}
|
|
1984
|
+
function boulderLine(result) {
|
|
1985
|
+
const active = !result.ready && result.reason === "boulder-active";
|
|
1986
|
+
return active ? "<b>Boulder</b>: \uD65C\uC131" : "<b>Boulder</b>: \uC5C6\uC74C";
|
|
1987
|
+
}
|
|
1988
|
+
function createStatusDispatcher(deps) {
|
|
1989
|
+
return async ({ chatId, bot, args }) => {
|
|
1990
|
+
const rawN = args[0];
|
|
1991
|
+
if (rawN === void 0 || rawN === "") {
|
|
1992
|
+
await bot.sendMessage("\uC0AC\uC6A9\uBC95: /status <\uBC88\uD638>. \uBA3C\uC800 /sessions \uB85C \uBAA9\uB85D \uD655\uC778", {
|
|
1993
|
+
parse_mode: "HTML"
|
|
1994
|
+
});
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const n = Number(rawN);
|
|
1998
|
+
if (Number.isNaN(n)) {
|
|
1999
|
+
await bot.sendMessage(`\uC798\uBABB\uB41C \uC785\uB825: ${escapeHtml(rawN)}\uC740 \uC22B\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4`, {
|
|
2000
|
+
parse_mode: "HTML"
|
|
2001
|
+
});
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
const snapshot = await deps.snapshotStore.loadSnapshot(chatId);
|
|
2005
|
+
if (!snapshot) {
|
|
2006
|
+
await bot.sendMessage("\uC138\uC158 \uBAA9\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 /sessions \uB97C \uC2E4\uD589\uD558\uC138\uC694.", {
|
|
2007
|
+
parse_mode: "HTML"
|
|
2008
|
+
});
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const entry = snapshot.find((e) => e.index === n);
|
|
2012
|
+
if (!entry) {
|
|
2013
|
+
await bot.sendMessage(`${n}\uBC88 \uC138\uC158 \uC5C6\uC74C. \uD604\uC7AC \uBAA9\uB85D \uD06C\uAE30: ${snapshot.length}`, {
|
|
2014
|
+
parse_mode: "HTML"
|
|
2015
|
+
});
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
const rawSourceServerUrl = entry.serverUrl ?? deps.sessionTitleService.getServerUrl(entry.sessionId);
|
|
2019
|
+
const sourceServerUrl = normalizeOpenCodeServerUrl(rawSourceServerUrl);
|
|
2020
|
+
if (rawSourceServerUrl && !sourceServerUrl) {
|
|
2021
|
+
await bot.sendMessage("\uC138\uC158 \uC11C\uBC84 \uC815\uBCF4\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
|
|
2022
|
+
parse_mode: "HTML"
|
|
2023
|
+
});
|
|
2024
|
+
deps.logger.error("status invalid server url", { chatId, sessionId: entry.sessionId });
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const useRemoteServer = isDifferentServerUrl(sourceServerUrl, deps.serverUrl);
|
|
2028
|
+
let session;
|
|
2029
|
+
let responseStatus;
|
|
2030
|
+
let sessionStatus = "idle";
|
|
2031
|
+
let messages = [];
|
|
2032
|
+
if (sourceServerUrl && useRemoteServer) {
|
|
2033
|
+
try {
|
|
2034
|
+
const getResult = await getRemoteSession(sourceServerUrl, entry.sessionId, deps.opencodeFetch);
|
|
2035
|
+
session = getResult.data;
|
|
2036
|
+
responseStatus = getResult.response.status;
|
|
2037
|
+
if (!session || responseStatus === 404) {
|
|
2038
|
+
await bot.sendMessage("\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
|
|
2039
|
+
parse_mode: "HTML"
|
|
2040
|
+
});
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
const [statusMap, remoteMessages] = await Promise.all([
|
|
2044
|
+
getRemoteStatusMap(sourceServerUrl, deps.opencodeFetch),
|
|
2045
|
+
getRemoteMessages(sourceServerUrl, entry.sessionId, MESSAGES_LIMIT, deps.opencodeFetch)
|
|
2046
|
+
]);
|
|
2047
|
+
sessionStatus = statusMap[entry.sessionId]?.type ?? "idle";
|
|
2048
|
+
messages = remoteMessages;
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
await bot.sendMessage("\uC138\uC158 \uC0C1\uD0DC\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
|
|
2051
|
+
parse_mode: "HTML"
|
|
2052
|
+
});
|
|
2053
|
+
deps.logger.error("status remote lookup failed", { chatId, sessionId: entry.sessionId, error: String(err) });
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
} else {
|
|
2057
|
+
const [getResult, statusResult, messagesResult] = await Promise.all([
|
|
2058
|
+
deps.client.session.get({ path: { id: entry.sessionId } }),
|
|
2059
|
+
deps.client.session.status(),
|
|
2060
|
+
deps.client.session.messages({
|
|
2061
|
+
path: { id: entry.sessionId },
|
|
2062
|
+
query: { limit: MESSAGES_LIMIT }
|
|
2063
|
+
})
|
|
2064
|
+
]);
|
|
2065
|
+
session = normalizeSession(getResult.data);
|
|
2066
|
+
responseStatus = getResult.response?.status;
|
|
2067
|
+
const statusMap = normalizeStatusMap(statusResult.data);
|
|
2068
|
+
sessionStatus = statusMap[entry.sessionId]?.type ?? "idle";
|
|
2069
|
+
messages = normalizeMessages(messagesResult.data);
|
|
2070
|
+
}
|
|
2071
|
+
if (!session || responseStatus === 404) {
|
|
2072
|
+
await bot.sendMessage("\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
|
|
2073
|
+
parse_mode: "HTML"
|
|
2074
|
+
});
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const projectRoot = resolveProjectRoot(session);
|
|
2078
|
+
const planReady = await checkPlanReadiness({ projectRoot });
|
|
2079
|
+
const userSnippet = buildSnippet(findLastByRole(messages, "user"));
|
|
2080
|
+
const assistantSnippet = buildSnippet(findLastByRole(messages, "assistant"));
|
|
2081
|
+
const title = escapeHtml(session.title ?? "");
|
|
2082
|
+
const agent = entry.agent ? escapeHtml(entry.agent) : "?";
|
|
2083
|
+
const text = [
|
|
2084
|
+
`<b>\uC138\uC158 #${n}</b>: ${title}`,
|
|
2085
|
+
`\uC5D0\uC774\uC804\uD2B8: ${agent}`,
|
|
2086
|
+
`\uC0C1\uD0DC: ${escapeHtml(sessionStatus)}`,
|
|
2087
|
+
``,
|
|
2088
|
+
`<b>\uB9C8\uC9C0\uB9C9 \uBA54\uC2DC\uC9C0</b>`,
|
|
2089
|
+
`\uC720\uC800: ${userSnippet}`,
|
|
2090
|
+
`\uC5D0\uC774\uC804\uD2B8: ${assistantSnippet}`,
|
|
2091
|
+
``,
|
|
2092
|
+
planLine(planReady),
|
|
2093
|
+
``,
|
|
2094
|
+
boulderLine(planReady)
|
|
2095
|
+
].join("\n");
|
|
2096
|
+
await bot.sendMessage(text, { parse_mode: "HTML" });
|
|
2097
|
+
deps.logger.info("status shown", {
|
|
2098
|
+
chatId,
|
|
2099
|
+
sessionId: entry.sessionId,
|
|
2100
|
+
snapshotIndex: n
|
|
2101
|
+
});
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/events/start-work-command.ts
|
|
2106
|
+
function agentFromSession3(session) {
|
|
2107
|
+
return session.agent;
|
|
2108
|
+
}
|
|
2109
|
+
function resolveProjectRoot2(session) {
|
|
2110
|
+
return session.directory;
|
|
2111
|
+
}
|
|
2112
|
+
function readinessMessage(reason) {
|
|
2113
|
+
switch (reason) {
|
|
2114
|
+
case "no-omo-dir":
|
|
2115
|
+
return ".omo/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. plan \uC791\uC131\uC774 \uC120\uD589\uB418\uC5B4\uC57C \uD569\uB2C8\uB2E4";
|
|
2116
|
+
case "no-plans":
|
|
2117
|
+
return ".omo/plans/ \uC5D0 plan \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4";
|
|
2118
|
+
case "plan-empty":
|
|
2119
|
+
return "plan \uD30C\uC77C\uC5D0 \uCCB4\uD06C\uBC15\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4 (\uD5E4\uB354\uB9CC \uC874\uC7AC)";
|
|
2120
|
+
case "all-plans-complete":
|
|
2121
|
+
return "plan \uC758 \uBAA8\uB4E0 task \uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC0C8 plan \uC791\uC131 \uD544\uC694";
|
|
2122
|
+
case "boulder-active":
|
|
2123
|
+
return ".omo/boulder.json \uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uAE30\uC874 \uC791\uC5C5\uC774 \uC9C4\uD589 \uC911\uC774\uAC70\uB098 archive \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4";
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
function isSessionNotFoundError(err) {
|
|
2127
|
+
const httpError = err;
|
|
2128
|
+
return httpError.status === 404 || httpError.statusCode === 404 || httpError.response?.status === 404 || err.message.includes("404");
|
|
2129
|
+
}
|
|
2130
|
+
async function sendHtml(bot, text) {
|
|
2131
|
+
await bot.sendMessage(text, { parse_mode: "HTML" });
|
|
2132
|
+
}
|
|
2133
|
+
async function sendPlain(bot, text) {
|
|
2134
|
+
await bot.sendMessage(text);
|
|
2135
|
+
}
|
|
2136
|
+
function createStartWorkCommandDispatcher(deps) {
|
|
2137
|
+
return async ({ chatId, bot, args }) => {
|
|
2138
|
+
const rawIndex = args[0]?.trim();
|
|
2139
|
+
if (!rawIndex) {
|
|
2140
|
+
await sendPlain(bot, "\uC0AC\uC6A9\uBC95: /start_work <\uBC88\uD638>. \uBA3C\uC800 /sessions \uB85C \uBAA9\uB85D \uD655\uC778");
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
const index = Number(rawIndex);
|
|
2144
|
+
if (Number.isNaN(index)) {
|
|
2145
|
+
await sendPlain(bot, `\uC798\uBABB\uB41C \uC785\uB825: ${rawIndex}\uC740 \uC22B\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4`);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const snapshot = await deps.snapshotStore.loadSnapshot(chatId);
|
|
2149
|
+
if (snapshot === null) {
|
|
2150
|
+
await sendPlain(bot, "\uC138\uC158 \uBAA9\uB85D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 /sessions \uC2E4\uD589");
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const entry = snapshot.find((candidate) => candidate.index === index);
|
|
2154
|
+
if (!entry) {
|
|
2155
|
+
await sendPlain(bot, `${index}\uBC88 \uC138\uC158 \uC5C6\uC74C (\uBAA9\uB85D \uD06C\uAE30: ${snapshot.length})`);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
const sessionId = entry.sessionId;
|
|
2159
|
+
const rawSourceServerUrl = entry.serverUrl ?? deps.sessionTitleService.getServerUrl(sessionId);
|
|
2160
|
+
const sourceServerUrl = normalizeOpenCodeServerUrl(rawSourceServerUrl);
|
|
2161
|
+
if (rawSourceServerUrl && !sourceServerUrl) {
|
|
2162
|
+
await sendPlain(bot, "\uC138\uC158 \uC11C\uBC84 \uC815\uBCF4\uAC00 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
|
|
2163
|
+
deps.logger.error("start-work invalid server url", { sessionId });
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const useRemoteServer = isDifferentServerUrl(sourceServerUrl, deps.serverUrl);
|
|
2167
|
+
let session;
|
|
2168
|
+
try {
|
|
2169
|
+
if (sourceServerUrl && useRemoteServer) {
|
|
2170
|
+
const result = await getRemoteSession(sourceServerUrl, sessionId, deps.opencodeFetch);
|
|
2171
|
+
if (!result.data || result.response.status === 404) {
|
|
2172
|
+
await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
session = result.data;
|
|
2176
|
+
} else {
|
|
2177
|
+
const result = await deps.client.session.get({ path: { id: sessionId } });
|
|
2178
|
+
if (!result.data) {
|
|
2179
|
+
await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
session = result.data;
|
|
2183
|
+
}
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
if (err instanceof Error && isSessionNotFoundError(err)) {
|
|
2186
|
+
await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
await sendPlain(bot, "\uC138\uC158 \uD655\uC778 \uC2E4\uD328. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
|
|
2190
|
+
deps.logger.error("start-work session lookup failed", { sessionId, error: String(err) });
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession3(session) ?? entry.agent;
|
|
2194
|
+
if (agent !== "plan") {
|
|
2195
|
+
await sendPlain(
|
|
2196
|
+
bot,
|
|
2197
|
+
`${index}\uBC88 \uC138\uC158\uC758 \uC5D0\uC774\uC804\uD2B8\uB294 'plan' \uC774 \uC544\uB2D9\uB2C8\uB2E4 (\uD604\uC7AC: ${agent ?? "unknown"}). /start_work \uB294 plan \uC138\uC158\uC5D0\uC11C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4`
|
|
2198
|
+
);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
let idle;
|
|
2202
|
+
try {
|
|
2203
|
+
idle = sourceServerUrl && useRemoteServer ? ((await getRemoteStatusMap(sourceServerUrl, deps.opencodeFetch))[sessionId]?.type ?? "idle") === "idle" : await recheckSessionIdle(deps.client, sessionId);
|
|
2204
|
+
} catch (err) {
|
|
2205
|
+
await sendPlain(bot, "\uC138\uC158 \uC0C1\uD0DC \uD655\uC778 \uC2E4\uD328. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
|
|
2206
|
+
deps.logger.error("start-work idle recheck failed", { sessionId, error: String(err) });
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (!idle) {
|
|
2210
|
+
await sendPlain(bot, `${index}\uBC88 \uC138\uC158\uC774 idle \uC0C1\uD0DC\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uC791\uC5C5 \uC644\uB8CC\uB97C \uAE30\uB2E4\uB9AC\uC138\uC694`);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
const readiness = await checkPlanReadiness({ projectRoot: resolveProjectRoot2(session) });
|
|
2214
|
+
if (!readiness.ready) {
|
|
2215
|
+
await sendPlain(bot, readinessMessage(readiness.reason));
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
try {
|
|
2219
|
+
await deps.runSessionCommand(sessionId, "start-work", sourceServerUrl);
|
|
2220
|
+
await sendHtml(
|
|
2221
|
+
bot,
|
|
2222
|
+
`${index}\uBC88 \uC138\uC158\uC5D0 opencode /start-work \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1 \uC644\uB8CC. (${escapeHtml(entry.title)})`
|
|
2223
|
+
);
|
|
2224
|
+
deps.logger.info("start-work dispatched", { chatId, sessionId, index });
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
await sendHtml(bot, "opencode /start-work \uC804\uC1A1 \uC2E4\uD328");
|
|
2227
|
+
deps.logger.error("start-work dispatch failed", { sessionId, error: String(err) });
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// src/events/help-command.ts
|
|
2233
|
+
var HELP_TEXT = `<b>OpenCode Telegram Plugin \u2014 \uBA85\uB839 \uB3C4\uC6C0\uB9D0</b>
|
|
2234
|
+
|
|
2235
|
+
<b>/sessions</b>
|
|
2236
|
+
\uD65C\uC131 root \uC138\uC158 \uBAA9\uB85D\uC744 \uBC88\uD638\uC640 \uD568\uAED8 \uD45C\uC2DC (\uCD5C\uADFC\uD65C\uB3D9\uC21C top 20).
|
|
2237
|
+
|
|
2238
|
+
<b>/status <\uBC88\uD638></b>
|
|
2239
|
+
\uD574\uB2F9 \uC138\uC158\uC758 \uC5D0\uC774\uC804\uD2B8/\uC0C1\uD0DC/\uB9C8\uC9C0\uB9C9 \uBA54\uC2DC\uC9C0 \uC2A4\uB2C8\uD3AB/\uD50C\uB79C \uC9C4\uD589\uB3C4/boulder \uC0C1\uD0DC \uD45C\uC2DC.
|
|
2240
|
+
|
|
2241
|
+
<b>/start_work <\uBC88\uD638></b>
|
|
2242
|
+
\uD574\uB2F9 \uC138\uC158\uC5D0 opencode <code>/start-work</code> \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1.
|
|
2243
|
+
\uC548\uC804 \uAC8C\uC774\uD2B8: agent='plan' AND status=idle AND .omo/plans \uC5D0 \uBBF8\uC644\uB8CC plan \uC874\uC7AC AND .omo/boulder.json \uBD80\uC7AC.
|
|
2244
|
+
\uC870\uAC74 \uBBF8\uCDA9\uC871\uC2DC \uAD6C\uCCB4\uC801 \uC0AC\uC720 \uC548\uB0B4.
|
|
2245
|
+
(Telegram \uBD07 \uBA85\uB839\uC740 <code>/start_work</code>, \uB0B4\uBD80 \uD2B8\uB9AC\uAC70 \uB300\uC0C1\uC740 opencode \uC758 <code>/start-work</code>)
|
|
2246
|
+
|
|
2247
|
+
<b>/help</b>
|
|
2248
|
+
\uC774 \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC.
|
|
2249
|
+
|
|
2250
|
+
<b>\uC81C\uC57D</b>
|
|
2251
|
+
\uBC88\uD638\uB294 <code>/sessions</code> \uB9C8\uC9C0\uB9C9 \uD638\uCD9C\uC758 \uC2A4\uB0C5\uC0F7\uC5D0 \uC885\uC18D (TTL 1\uC2DC\uAC04).
|
|
2252
|
+
leader \uD504\uB85C\uC138\uC2A4\uAC00 \uAD00\uCC30\uD55C \uC138\uC158\uB9CC \uD45C\uC2DC \u2014 \uB2E4\uB978 OpenCode \uD504\uB85C\uC138\uC2A4\uC758 \uC138\uC158\uC740 \uBCF4\uC774\uC9C0 \uC54A\uC744 \uC218 \uC788\uC74C.`;
|
|
2253
|
+
function createHelpDispatcher(deps) {
|
|
2254
|
+
return async ({ chatId, bot }) => {
|
|
2255
|
+
await bot.sendMessage(HELP_TEXT, { parse_mode: "HTML" });
|
|
2256
|
+
deps.logger.info("help shown", { chatId });
|
|
2257
|
+
};
|
|
1294
2258
|
}
|
|
1295
2259
|
|
|
1296
2260
|
// src/lib/env-loader.ts
|
|
1297
2261
|
import { existsSync } from "fs";
|
|
1298
2262
|
import { homedir } from "os";
|
|
1299
|
-
import { join as
|
|
2263
|
+
import { join as join7 } from "path";
|
|
1300
2264
|
import dotenv from "dotenv";
|
|
1301
2265
|
function loadPluginEnv(opts) {
|
|
1302
2266
|
const paths = [
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
2267
|
+
join7(opts.pluginDir, "../../.env"),
|
|
2268
|
+
join7(opts.pluginDir, "..", ".env"),
|
|
2269
|
+
join7(opts.pluginDir, ".env"),
|
|
2270
|
+
join7(opts.homeDir ?? homedir(), ".config/opencode/telegram-remote/.env")
|
|
1307
2271
|
];
|
|
1308
2272
|
const loadedFrom = [];
|
|
1309
2273
|
const values = {};
|
|
@@ -1321,10 +2285,10 @@ function loadPluginEnv(opts) {
|
|
|
1321
2285
|
}
|
|
1322
2286
|
|
|
1323
2287
|
// src/lib/lock.ts
|
|
1324
|
-
import { open as open2, readFile as
|
|
2288
|
+
import { open as open2, readFile as readFile6, stat as stat3, unlink as unlink6 } from "fs/promises";
|
|
1325
2289
|
import { hostname } from "os";
|
|
1326
2290
|
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
1327
|
-
function
|
|
2291
|
+
function hasCode6(err, code) {
|
|
1328
2292
|
return "code" in err && err.code === code;
|
|
1329
2293
|
}
|
|
1330
2294
|
function parseLockData(text) {
|
|
@@ -1343,7 +2307,7 @@ function isPidAlive(pid) {
|
|
|
1343
2307
|
process.kill(pid, 0);
|
|
1344
2308
|
return true;
|
|
1345
2309
|
} catch (err) {
|
|
1346
|
-
if (err instanceof Error &&
|
|
2310
|
+
if (err instanceof Error && hasCode6(err, "ESRCH")) return false;
|
|
1347
2311
|
return true;
|
|
1348
2312
|
}
|
|
1349
2313
|
}
|
|
@@ -1364,7 +2328,7 @@ async function createLock(lockPath, pid) {
|
|
|
1364
2328
|
if (released) return;
|
|
1365
2329
|
released = true;
|
|
1366
2330
|
try {
|
|
1367
|
-
await
|
|
2331
|
+
await unlink6(lockPath);
|
|
1368
2332
|
} catch {
|
|
1369
2333
|
}
|
|
1370
2334
|
}
|
|
@@ -1374,7 +2338,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1374
2338
|
let ownerPid;
|
|
1375
2339
|
let dead = false;
|
|
1376
2340
|
try {
|
|
1377
|
-
const text = await
|
|
2341
|
+
const text = await readFile6(lockPath, "utf8");
|
|
1378
2342
|
const data = parseLockData(text);
|
|
1379
2343
|
if (data) {
|
|
1380
2344
|
ownerPid = data.pid;
|
|
@@ -1384,7 +2348,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1384
2348
|
return { stale: true, reason: "unreadable lock" };
|
|
1385
2349
|
}
|
|
1386
2350
|
try {
|
|
1387
|
-
const fileStat = await
|
|
2351
|
+
const fileStat = await stat3(lockPath);
|
|
1388
2352
|
const expired = Date.now() - fileStat.mtimeMs > ttlMs;
|
|
1389
2353
|
if (dead) return { stale: true, ownerPid, reason: "dead owner" };
|
|
1390
2354
|
if (expired) return { stale: true, ownerPid, reason: "expired lock" };
|
|
@@ -1400,7 +2364,7 @@ async function acquireLock(opts) {
|
|
|
1400
2364
|
try {
|
|
1401
2365
|
return { acquired: true, handle: await createLock(opts.lockPath, pid) };
|
|
1402
2366
|
} catch (err) {
|
|
1403
|
-
if (!(err instanceof Error) || !
|
|
2367
|
+
if (!(err instanceof Error) || !hasCode6(err, "EEXIST")) {
|
|
1404
2368
|
return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1405
2369
|
}
|
|
1406
2370
|
const existing = await inspectExisting(opts.lockPath, ttlMs);
|
|
@@ -1408,7 +2372,7 @@ async function acquireLock(opts) {
|
|
|
1408
2372
|
return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
|
|
1409
2373
|
}
|
|
1410
2374
|
try {
|
|
1411
|
-
await
|
|
2375
|
+
await unlink6(opts.lockPath);
|
|
1412
2376
|
} catch {
|
|
1413
2377
|
return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
|
|
1414
2378
|
}
|
|
@@ -1487,11 +2451,152 @@ function createLogger(opts = {}) {
|
|
|
1487
2451
|
};
|
|
1488
2452
|
}
|
|
1489
2453
|
|
|
2454
|
+
// src/lib/session-snapshot.ts
|
|
2455
|
+
import { chmod as chmod2, mkdir as mkdir6, readFile as readFile7, rename as rename5, unlink as unlink7, writeFile as writeFile5 } from "fs/promises";
|
|
2456
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
2457
|
+
var TTL_MS = 60 * 60 * 1e3;
|
|
2458
|
+
function hasCode7(err, code) {
|
|
2459
|
+
return err instanceof Error && "code" in err && err.code === code;
|
|
2460
|
+
}
|
|
2461
|
+
function isSnapshotFile(value) {
|
|
2462
|
+
if (!value || typeof value !== "object") return false;
|
|
2463
|
+
const v = value;
|
|
2464
|
+
if (v.version !== 1) return false;
|
|
2465
|
+
if (typeof v.chatId !== "number") return false;
|
|
2466
|
+
if (typeof v.createdAt !== "number") return false;
|
|
2467
|
+
if (!Array.isArray(v.entries)) return false;
|
|
2468
|
+
for (const entry of v.entries) {
|
|
2469
|
+
if (!entry || typeof entry !== "object") return false;
|
|
2470
|
+
const e = entry;
|
|
2471
|
+
if (typeof e.index !== "number") return false;
|
|
2472
|
+
if (typeof e.sessionId !== "string") return false;
|
|
2473
|
+
if (typeof e.title !== "string") return false;
|
|
2474
|
+
if (typeof e.capturedAt !== "number") return false;
|
|
2475
|
+
if (e.agent !== void 0 && typeof e.agent !== "string") return false;
|
|
2476
|
+
if (e.status !== void 0 && typeof e.status !== "string") return false;
|
|
2477
|
+
if (e.serverUrl !== void 0) {
|
|
2478
|
+
if (typeof e.serverUrl !== "string") return false;
|
|
2479
|
+
if (!normalizeOpenCodeServerUrl(e.serverUrl)) return false;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return true;
|
|
2483
|
+
}
|
|
2484
|
+
function normalizeEntry2(entry) {
|
|
2485
|
+
const out = {
|
|
2486
|
+
index: entry.index,
|
|
2487
|
+
sessionId: entry.sessionId,
|
|
2488
|
+
title: entry.title,
|
|
2489
|
+
capturedAt: entry.capturedAt
|
|
2490
|
+
};
|
|
2491
|
+
if (entry.agent !== void 0) out.agent = entry.agent;
|
|
2492
|
+
if (entry.status !== void 0) out.status = entry.status;
|
|
2493
|
+
if (entry.serverUrl !== void 0) {
|
|
2494
|
+
const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
|
|
2495
|
+
if (serverUrl !== void 0) out.serverUrl = serverUrl;
|
|
2496
|
+
}
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
function createSnapshotStore(opts) {
|
|
2500
|
+
const { configDir, tokenHash, logger } = opts;
|
|
2501
|
+
const snapshotsDir = join8(configDir, "snapshots");
|
|
2502
|
+
const writeLocks = /* @__PURE__ */ new Map();
|
|
2503
|
+
function snapshotFilePath(chatId) {
|
|
2504
|
+
return join8(snapshotsDir, `${tokenHash}-${chatId}.json`);
|
|
2505
|
+
}
|
|
2506
|
+
async function performSave(chatId, entries) {
|
|
2507
|
+
const filePath = snapshotFilePath(chatId);
|
|
2508
|
+
const parent = dirname4(filePath);
|
|
2509
|
+
await mkdir6(parent, { recursive: true });
|
|
2510
|
+
try {
|
|
2511
|
+
await chmod2(parent, 448);
|
|
2512
|
+
} catch (err) {
|
|
2513
|
+
logger.error("snapshot: failed to chmod parent dir", {
|
|
2514
|
+
path: parent,
|
|
2515
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2516
|
+
});
|
|
2517
|
+
}
|
|
2518
|
+
const payload = {
|
|
2519
|
+
version: 1,
|
|
2520
|
+
chatId,
|
|
2521
|
+
createdAt: Date.now(),
|
|
2522
|
+
entries: entries.map(normalizeEntry2)
|
|
2523
|
+
};
|
|
2524
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
2525
|
+
await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
2526
|
+
try {
|
|
2527
|
+
await rename5(tmpPath, filePath);
|
|
2528
|
+
} catch (err) {
|
|
2529
|
+
try {
|
|
2530
|
+
await unlink7(tmpPath);
|
|
2531
|
+
} catch {
|
|
2532
|
+
}
|
|
2533
|
+
throw err;
|
|
2534
|
+
}
|
|
2535
|
+
await chmod2(filePath, 384);
|
|
2536
|
+
}
|
|
2537
|
+
async function saveSnapshot(chatId, entries) {
|
|
2538
|
+
const prev = writeLocks.get(chatId) ?? Promise.resolve();
|
|
2539
|
+
const next = prev.catch(() => void 0).then(() => performSave(chatId, entries));
|
|
2540
|
+
const tracked = next.catch(() => void 0);
|
|
2541
|
+
writeLocks.set(chatId, tracked);
|
|
2542
|
+
try {
|
|
2543
|
+
await next;
|
|
2544
|
+
} finally {
|
|
2545
|
+
if (writeLocks.get(chatId) === tracked) {
|
|
2546
|
+
writeLocks.delete(chatId);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
async function loadSnapshot(chatId) {
|
|
2551
|
+
const filePath = snapshotFilePath(chatId);
|
|
2552
|
+
let text;
|
|
2553
|
+
try {
|
|
2554
|
+
text = await readFile7(filePath, "utf8");
|
|
2555
|
+
} catch (err) {
|
|
2556
|
+
if (hasCode7(err, "ENOENT")) return null;
|
|
2557
|
+
logger.error("snapshot: failed to read file", {
|
|
2558
|
+
path: filePath,
|
|
2559
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2560
|
+
});
|
|
2561
|
+
return null;
|
|
2562
|
+
}
|
|
2563
|
+
let parsed;
|
|
2564
|
+
try {
|
|
2565
|
+
parsed = JSON.parse(text);
|
|
2566
|
+
} catch (err) {
|
|
2567
|
+
logger.error("snapshot: corrupted JSON", {
|
|
2568
|
+
path: filePath,
|
|
2569
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2570
|
+
});
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
if (!isSnapshotFile(parsed)) {
|
|
2574
|
+
logger.error("snapshot: invalid shape", { path: filePath });
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
if (parsed.createdAt + TTL_MS < Date.now()) {
|
|
2578
|
+
try {
|
|
2579
|
+
await unlink7(filePath);
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
if (!hasCode7(err, "ENOENT")) {
|
|
2582
|
+
logger.error("snapshot: failed to unlink expired file", {
|
|
2583
|
+
path: filePath,
|
|
2584
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return null;
|
|
2589
|
+
}
|
|
2590
|
+
return parsed.entries.map(normalizeEntry2);
|
|
2591
|
+
}
|
|
2592
|
+
return { saveSnapshot, loadSnapshot, snapshotFilePath };
|
|
2593
|
+
}
|
|
2594
|
+
|
|
1490
2595
|
// src/lib/state-store.ts
|
|
1491
|
-
import { mkdir as
|
|
2596
|
+
import { mkdir as mkdir7, readFile as readFile8, rename as rename6, writeFile as writeFile6 } from "fs/promises";
|
|
1492
2597
|
import { homedir as homedir2 } from "os";
|
|
1493
|
-
import { dirname as
|
|
1494
|
-
function
|
|
2598
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
2599
|
+
function hasCode8(err, code) {
|
|
1495
2600
|
return "code" in err && err.code === code;
|
|
1496
2601
|
}
|
|
1497
2602
|
function parseState(text) {
|
|
@@ -1503,28 +2608,28 @@ function parseState(text) {
|
|
|
1503
2608
|
return state;
|
|
1504
2609
|
}
|
|
1505
2610
|
function createStateStore(opts = {}) {
|
|
1506
|
-
const filePath = opts.filePath ??
|
|
2611
|
+
const filePath = opts.filePath ?? join9(homedir2(), ".config/opencode/telegram-remote/state.json");
|
|
1507
2612
|
return {
|
|
1508
2613
|
async read() {
|
|
1509
2614
|
try {
|
|
1510
|
-
return parseState(await
|
|
2615
|
+
return parseState(await readFile8(filePath, "utf8"));
|
|
1511
2616
|
} catch (err) {
|
|
1512
|
-
if (err instanceof Error &&
|
|
2617
|
+
if (err instanceof Error && hasCode8(err, "ENOENT")) return {};
|
|
1513
2618
|
throw err;
|
|
1514
2619
|
}
|
|
1515
2620
|
},
|
|
1516
2621
|
async write(patch) {
|
|
1517
2622
|
const existing = await this.read();
|
|
1518
2623
|
const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1519
|
-
await
|
|
2624
|
+
await mkdir7(dirname5(filePath), { recursive: true });
|
|
1520
2625
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1521
|
-
await
|
|
2626
|
+
await writeFile6(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
1522
2627
|
try {
|
|
1523
|
-
await
|
|
2628
|
+
await rename6(tmpPath, filePath);
|
|
1524
2629
|
} catch (err) {
|
|
1525
|
-
if (!(err instanceof Error) || !
|
|
1526
|
-
await
|
|
1527
|
-
await
|
|
2630
|
+
if (!(err instanceof Error) || !hasCode8(err, "ENOENT")) throw err;
|
|
2631
|
+
await writeFile6(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
2632
|
+
await rename6(tmpPath, filePath);
|
|
1528
2633
|
}
|
|
1529
2634
|
return next;
|
|
1530
2635
|
}
|
|
@@ -1532,7 +2637,7 @@ function createStateStore(opts = {}) {
|
|
|
1532
2637
|
}
|
|
1533
2638
|
|
|
1534
2639
|
// src/services/session-title-service.ts
|
|
1535
|
-
function
|
|
2640
|
+
function agentFromSession4(info) {
|
|
1536
2641
|
const candidate = info;
|
|
1537
2642
|
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
1538
2643
|
}
|
|
@@ -1543,9 +2648,11 @@ var SessionTitleService = class {
|
|
|
1543
2648
|
this.sessions.set(info.id, {
|
|
1544
2649
|
title: info.title || null,
|
|
1545
2650
|
parentID: info.parentID ?? null,
|
|
1546
|
-
agent:
|
|
2651
|
+
agent: agentFromSession4(info) ?? existing?.agent,
|
|
1547
2652
|
status: existing?.status,
|
|
1548
|
-
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
2653
|
+
idleNotificationPending: existing?.idleNotificationPending ?? false,
|
|
2654
|
+
lastSeenAt: Date.now(),
|
|
2655
|
+
serverUrl: existing?.serverUrl
|
|
1549
2656
|
});
|
|
1550
2657
|
}
|
|
1551
2658
|
setSessionTitle(sessionId, title) {
|
|
@@ -1555,7 +2662,9 @@ var SessionTitleService = class {
|
|
|
1555
2662
|
parentID: existing?.parentID,
|
|
1556
2663
|
agent: existing?.agent,
|
|
1557
2664
|
status: existing?.status,
|
|
1558
|
-
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
2665
|
+
idleNotificationPending: existing?.idleNotificationPending ?? false,
|
|
2666
|
+
lastSeenAt: Date.now(),
|
|
2667
|
+
serverUrl: existing?.serverUrl
|
|
1559
2668
|
});
|
|
1560
2669
|
}
|
|
1561
2670
|
setSessionAgent(sessionId, agent) {
|
|
@@ -1565,7 +2674,9 @@ var SessionTitleService = class {
|
|
|
1565
2674
|
parentID: existing?.parentID,
|
|
1566
2675
|
agent,
|
|
1567
2676
|
status: existing?.status,
|
|
1568
|
-
idleNotificationPending: existing?.idleNotificationPending ?? false
|
|
2677
|
+
idleNotificationPending: existing?.idleNotificationPending ?? false,
|
|
2678
|
+
lastSeenAt: Date.now(),
|
|
2679
|
+
serverUrl: existing?.serverUrl
|
|
1569
2680
|
});
|
|
1570
2681
|
}
|
|
1571
2682
|
setSessionStatus(sessionId, status) {
|
|
@@ -1575,9 +2686,48 @@ var SessionTitleService = class {
|
|
|
1575
2686
|
parentID: existing?.parentID,
|
|
1576
2687
|
agent: existing?.agent,
|
|
1577
2688
|
status,
|
|
1578
|
-
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false
|
|
2689
|
+
idleNotificationPending: status === "idle" ? existing?.idleNotificationPending ?? false : false,
|
|
2690
|
+
lastSeenAt: Date.now(),
|
|
2691
|
+
serverUrl: existing?.serverUrl
|
|
1579
2692
|
});
|
|
1580
2693
|
}
|
|
2694
|
+
setServerUrl(sessionId, serverUrl) {
|
|
2695
|
+
const existing = this.sessions.get(sessionId);
|
|
2696
|
+
if (existing?.serverUrl) return;
|
|
2697
|
+
const lastSeenAt = existing?.lastSeenAt ?? Date.now();
|
|
2698
|
+
this.sessions.set(sessionId, {
|
|
2699
|
+
...existing ?? {
|
|
2700
|
+
title: null,
|
|
2701
|
+
parentID: void 0,
|
|
2702
|
+
idleNotificationPending: false,
|
|
2703
|
+
lastSeenAt
|
|
2704
|
+
},
|
|
2705
|
+
lastSeenAt,
|
|
2706
|
+
serverUrl
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
getServerUrl(sessionId) {
|
|
2710
|
+
return this.sessions.get(sessionId)?.serverUrl;
|
|
2711
|
+
}
|
|
2712
|
+
getRootSessionsByRecency(limit) {
|
|
2713
|
+
const results = [];
|
|
2714
|
+
for (const [sessionId, entry] of this.sessions.entries()) {
|
|
2715
|
+
if (entry.parentID !== null) continue;
|
|
2716
|
+
results.push({
|
|
2717
|
+
sessionId,
|
|
2718
|
+
title: entry.title,
|
|
2719
|
+
agent: entry.agent,
|
|
2720
|
+
status: entry.status,
|
|
2721
|
+
serverUrl: entry.serverUrl
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
results.sort((a, b) => {
|
|
2725
|
+
const lastSeenA = this.sessions.get(a.sessionId)?.lastSeenAt ?? 0;
|
|
2726
|
+
const lastSeenB = this.sessions.get(b.sessionId)?.lastSeenAt ?? 0;
|
|
2727
|
+
return lastSeenB - lastSeenA;
|
|
2728
|
+
});
|
|
2729
|
+
return results.slice(0, limit);
|
|
2730
|
+
}
|
|
1581
2731
|
getSessionTitle(sessionId) {
|
|
1582
2732
|
return this.sessions.get(sessionId)?.title ?? null;
|
|
1583
2733
|
}
|
|
@@ -1605,7 +2755,9 @@ var SessionTitleService = class {
|
|
|
1605
2755
|
parentID: existing?.parentID,
|
|
1606
2756
|
agent: existing?.agent,
|
|
1607
2757
|
status: existing?.status ?? "idle",
|
|
1608
|
-
idleNotificationPending: true
|
|
2758
|
+
idleNotificationPending: true,
|
|
2759
|
+
lastSeenAt: existing?.lastSeenAt ?? Date.now(),
|
|
2760
|
+
serverUrl: existing?.serverUrl
|
|
1609
2761
|
});
|
|
1610
2762
|
}
|
|
1611
2763
|
hasDeferredIdleNotification(sessionId) {
|
|
@@ -1622,19 +2774,19 @@ var SessionTitleService = class {
|
|
|
1622
2774
|
};
|
|
1623
2775
|
|
|
1624
2776
|
// src/telegram-remote.ts
|
|
1625
|
-
var pluginDir =
|
|
2777
|
+
var pluginDir = dirname6(fileURLToPath(import.meta.url));
|
|
1626
2778
|
async function postToServer(serverUrl, path, body) {
|
|
1627
|
-
const
|
|
2779
|
+
const safeServerUrl = normalizeOpenCodeServerUrl(serverUrl);
|
|
2780
|
+
if (!safeServerUrl) throw new Error("Invalid OpenCode server URL");
|
|
2781
|
+
const url = new URL(path, safeServerUrl);
|
|
1628
2782
|
const response = await fetch(url, {
|
|
1629
2783
|
method: "POST",
|
|
1630
2784
|
headers: { "Content-Type": "application/json" },
|
|
1631
|
-
body: JSON.stringify(body)
|
|
2785
|
+
body: JSON.stringify(body),
|
|
2786
|
+
redirect: "error"
|
|
1632
2787
|
});
|
|
1633
2788
|
if (response.ok) return;
|
|
1634
|
-
|
|
1635
|
-
throw new Error(
|
|
1636
|
-
`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
|
|
1637
|
-
);
|
|
2789
|
+
throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
|
|
1638
2790
|
}
|
|
1639
2791
|
function getSessionAgentFromMessage(event) {
|
|
1640
2792
|
const info = event.properties.info;
|
|
@@ -1658,8 +2810,11 @@ var TelegramRemote = async (input) => {
|
|
|
1658
2810
|
const stateStore = createStateStore();
|
|
1659
2811
|
const initialState = await stateStore.read();
|
|
1660
2812
|
const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
2813
|
+
const configDir = join10(homedir3(), ".config/opencode/telegram-remote");
|
|
2814
|
+
const snapshotStore = createSnapshotStore({ configDir, tokenHash, logger });
|
|
2815
|
+
const sessionRegistry = createSessionRegistryStore({ configDir, tokenHash, logger });
|
|
2816
|
+
const lockPath = join10(tmpdir5(), `opencoder-telegram-${tokenHash}.lock`);
|
|
2817
|
+
const claimsDir = join10(tmpdir5(), `opencoder-telegram-claims-${tokenHash}`);
|
|
1663
2818
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
1664
2819
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
1665
2820
|
const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
|
|
@@ -1689,8 +2844,8 @@ var TelegramRemote = async (input) => {
|
|
|
1689
2844
|
throwOnError: true
|
|
1690
2845
|
});
|
|
1691
2846
|
};
|
|
1692
|
-
const replyToPermission = async (requestID, sessionID, reply,
|
|
1693
|
-
if (
|
|
2847
|
+
const replyToPermission = async (requestID, sessionID, reply, endpoint2, serverUrl = input.serverUrl.href) => {
|
|
2848
|
+
if (endpoint2 === "request") {
|
|
1694
2849
|
const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
|
|
1695
2850
|
if (serverUrl !== input.serverUrl.href) {
|
|
1696
2851
|
await postToServer(serverUrl, path2, { reply });
|
|
@@ -1774,6 +2929,7 @@ var TelegramRemote = async (input) => {
|
|
|
1774
2929
|
pendingQuestions,
|
|
1775
2930
|
pendingPermissions,
|
|
1776
2931
|
pendingStartWorks,
|
|
2932
|
+
sessionRegistry,
|
|
1777
2933
|
replyToQuestion,
|
|
1778
2934
|
replyToPermission,
|
|
1779
2935
|
runSessionCommand
|
|
@@ -1782,6 +2938,30 @@ var TelegramRemote = async (input) => {
|
|
|
1782
2938
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
1783
2939
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
1784
2940
|
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
2941
|
+
bot.setSessionsDispatcher(createSessionsDispatcher({
|
|
2942
|
+
client: input.client,
|
|
2943
|
+
sessionTitleService,
|
|
2944
|
+
sessionRegistry,
|
|
2945
|
+
snapshotStore,
|
|
2946
|
+
serverUrl: input.serverUrl.href,
|
|
2947
|
+
logger
|
|
2948
|
+
}));
|
|
2949
|
+
bot.setStatusDispatcher(createStatusDispatcher({
|
|
2950
|
+
snapshotStore,
|
|
2951
|
+
sessionTitleService,
|
|
2952
|
+
client: input.client,
|
|
2953
|
+
logger,
|
|
2954
|
+
serverUrl: input.serverUrl.href
|
|
2955
|
+
}));
|
|
2956
|
+
bot.setStartWorkCommandDispatcher(createStartWorkCommandDispatcher({
|
|
2957
|
+
snapshotStore,
|
|
2958
|
+
sessionTitleService,
|
|
2959
|
+
client: input.client,
|
|
2960
|
+
serverUrl: input.serverUrl.href,
|
|
2961
|
+
runSessionCommand,
|
|
2962
|
+
logger
|
|
2963
|
+
}));
|
|
2964
|
+
bot.setHelpDispatcher(createHelpDispatcher({ logger }));
|
|
1785
2965
|
}
|
|
1786
2966
|
return {
|
|
1787
2967
|
event: async ({ event }) => {
|
|
@@ -1793,13 +2973,25 @@ var TelegramRemote = async (input) => {
|
|
|
1793
2973
|
logger.info("session.status received", { statusType: event.properties.status.type });
|
|
1794
2974
|
return handleSessionStatus(event, ctx);
|
|
1795
2975
|
case "session.created":
|
|
2976
|
+
ctx.sessionTitleService.setServerUrl(event.properties.info.id, input.serverUrl.href);
|
|
1796
2977
|
return handleSessionCreated(event, ctx);
|
|
1797
2978
|
case "session.updated":
|
|
2979
|
+
ctx.sessionTitleService.setServerUrl(event.properties.info.id, input.serverUrl.href);
|
|
1798
2980
|
return handleSessionUpdated(event, ctx);
|
|
1799
2981
|
case "message.updated": {
|
|
1800
2982
|
const messageAgent = getSessionAgentFromMessage(event);
|
|
1801
|
-
if (messageAgent)
|
|
2983
|
+
if (messageAgent) {
|
|
2984
|
+
const previousAgent = ctx.sessionTitleService.getSessionAgent(messageAgent.sessionID);
|
|
1802
2985
|
ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
|
|
2986
|
+
ctx.sessionTitleService.setServerUrl(messageAgent.sessionID, input.serverUrl.href);
|
|
2987
|
+
if (previousAgent !== messageAgent.agent) {
|
|
2988
|
+
await ctx.sessionRegistry.updateSession(messageAgent.sessionID, {
|
|
2989
|
+
agent: messageAgent.agent,
|
|
2990
|
+
serverUrl: input.serverUrl.href,
|
|
2991
|
+
updatedAt: Date.now()
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
1803
2995
|
return;
|
|
1804
2996
|
}
|
|
1805
2997
|
case "permission.updated":
|
|
@@ -1807,7 +2999,16 @@ var TelegramRemote = async (input) => {
|
|
|
1807
2999
|
default: {
|
|
1808
3000
|
const stepAgent = getSessionAgentFromNextStep(extEvent);
|
|
1809
3001
|
if (stepAgent) {
|
|
3002
|
+
const previousAgent = ctx.sessionTitleService.getSessionAgent(stepAgent.sessionID);
|
|
1810
3003
|
ctx.sessionTitleService.setSessionAgent(stepAgent.sessionID, stepAgent.agent);
|
|
3004
|
+
ctx.sessionTitleService.setServerUrl(stepAgent.sessionID, input.serverUrl.href);
|
|
3005
|
+
if (previousAgent !== stepAgent.agent) {
|
|
3006
|
+
await ctx.sessionRegistry.updateSession(stepAgent.sessionID, {
|
|
3007
|
+
agent: stepAgent.agent,
|
|
3008
|
+
serverUrl: input.serverUrl.href,
|
|
3009
|
+
updatedAt: Date.now()
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
1811
3012
|
return;
|
|
1812
3013
|
}
|
|
1813
3014
|
if (isEventPermissionAsked(extEvent)) {
|