@coinseeker/opencode-telegram-plugin 1.0.13 → 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 +3 -3
- package/dist/telegram-remote.js +641 -128
- 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.14"]
|
|
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.14`.
|
|
23
23
|
|
|
24
24
|
Restart OpenCode after editing the config. OpenCode resolves npm package plugins on startup.
|
|
25
25
|
|
|
@@ -59,7 +59,7 @@ Keep this file private. Never commit or share your Telegram bot token.
|
|
|
59
59
|
- Permission approve/reject buttons from Telegram.
|
|
60
60
|
- Multi-session-safe Telegram polling through a file-lock leader model.
|
|
61
61
|
- Log file output instead of stdout terminal spam.
|
|
62
|
-
-
|
|
62
|
+
- Cross-process remote session listing via `/sessions`, `/status N`, `/start_work N`, `/help` slash commands.
|
|
63
63
|
- Safety-gated remote `/start-work` execution: verifies agent=plan, idle status, incomplete plan, and no active boulder before dispatching.
|
|
64
64
|
|
|
65
65
|
## Logs
|
package/dist/telegram-remote.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// src/telegram-remote.ts
|
|
7
7
|
import { createHash as createHash5 } from "crypto";
|
|
8
8
|
import { homedir as homedir3, tmpdir as tmpdir5 } from "os";
|
|
9
|
-
import { dirname as dirname6, join as
|
|
9
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
11
|
|
|
12
12
|
// src/bot.ts
|
|
@@ -1017,9 +1017,313 @@ async function handleQuestionReplied(event, ctx) {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
}
|
|
1019
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
|
+
|
|
1020
1320
|
// src/events/session-created.ts
|
|
1021
1321
|
async function handleSessionCreated(event, ctx) {
|
|
1022
|
-
|
|
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
|
+
);
|
|
1023
1327
|
}
|
|
1024
1328
|
|
|
1025
1329
|
// src/lib/abort-tracker.ts
|
|
@@ -1061,14 +1365,14 @@ async function handleSessionError(event, ctx) {
|
|
|
1061
1365
|
|
|
1062
1366
|
// src/lib/pending-start-work.ts
|
|
1063
1367
|
import { createHash as createHash4 } from "crypto";
|
|
1064
|
-
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";
|
|
1065
1369
|
import { tmpdir as tmpdir3 } from "os";
|
|
1066
|
-
import { dirname as dirname3, join as
|
|
1067
|
-
function
|
|
1370
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1371
|
+
function hasCode5(err, code) {
|
|
1068
1372
|
return "code" in err && err.code === code;
|
|
1069
1373
|
}
|
|
1070
1374
|
function pendingFilePath3(dir, shortHash) {
|
|
1071
|
-
return
|
|
1375
|
+
return join5(dir, `${shortHash}.json`);
|
|
1072
1376
|
}
|
|
1073
1377
|
function parsePending3(text) {
|
|
1074
1378
|
const parsed = JSON.parse(text);
|
|
@@ -1087,10 +1391,10 @@ function parsePending3(text) {
|
|
|
1087
1391
|
}
|
|
1088
1392
|
async function listPendingFiles3(dir) {
|
|
1089
1393
|
try {
|
|
1090
|
-
const entries = await
|
|
1394
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
1091
1395
|
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name);
|
|
1092
1396
|
} catch (err) {
|
|
1093
|
-
if (err instanceof Error &&
|
|
1397
|
+
if (err instanceof Error && hasCode5(err, "ENOENT")) return [];
|
|
1094
1398
|
throw err;
|
|
1095
1399
|
}
|
|
1096
1400
|
}
|
|
@@ -1098,29 +1402,29 @@ function shortHashFromFileName3(fileName) {
|
|
|
1098
1402
|
return fileName.slice(0, -".json".length);
|
|
1099
1403
|
}
|
|
1100
1404
|
function createPendingStartWorkStore(opts) {
|
|
1101
|
-
const dir = opts.baseDir ??
|
|
1405
|
+
const dir = opts.baseDir ?? join5(tmpdir3(), `opencoder-telegram-pending-start-work-${opts.tokenHash}`);
|
|
1102
1406
|
return {
|
|
1103
1407
|
dir,
|
|
1104
1408
|
async savePending(shortHash, data) {
|
|
1105
1409
|
const filePath = pendingFilePath3(dir, shortHash);
|
|
1106
|
-
await
|
|
1410
|
+
await mkdir5(dirname3(filePath), { recursive: true });
|
|
1107
1411
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
1108
|
-
await
|
|
1109
|
-
await
|
|
1412
|
+
await writeFile4(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
1413
|
+
await rename4(tmpPath, filePath);
|
|
1110
1414
|
},
|
|
1111
1415
|
async loadPending(shortHash) {
|
|
1112
1416
|
try {
|
|
1113
|
-
return parsePending3(await
|
|
1417
|
+
return parsePending3(await readFile4(pendingFilePath3(dir, shortHash), "utf8"));
|
|
1114
1418
|
} catch (err) {
|
|
1115
|
-
if (err instanceof Error &&
|
|
1419
|
+
if (err instanceof Error && hasCode5(err, "ENOENT")) return void 0;
|
|
1116
1420
|
throw err;
|
|
1117
1421
|
}
|
|
1118
1422
|
},
|
|
1119
1423
|
async deletePending(shortHash) {
|
|
1120
1424
|
try {
|
|
1121
|
-
await
|
|
1425
|
+
await unlink5(pendingFilePath3(dir, shortHash));
|
|
1122
1426
|
} catch (err) {
|
|
1123
|
-
if (!(err instanceof Error) || !
|
|
1427
|
+
if (!(err instanceof Error) || !hasCode5(err, "ENOENT")) throw err;
|
|
1124
1428
|
}
|
|
1125
1429
|
},
|
|
1126
1430
|
async sweepExpired() {
|
|
@@ -1228,6 +1532,13 @@ async function resolveParentID(sessionId, ctx) {
|
|
|
1228
1532
|
const result = await ctx.client.session.get({ path: { id: sessionId } });
|
|
1229
1533
|
if (result.data) {
|
|
1230
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
|
+
);
|
|
1231
1542
|
return ctx.sessionTitleService.getParentID(sessionId);
|
|
1232
1543
|
}
|
|
1233
1544
|
ctx.logger.warn("session parentID cache miss fetch returned no data", { sessionId });
|
|
@@ -1244,6 +1555,9 @@ async function hydrateDescendants(sessionId, ctx, seen = /* @__PURE__ */ new Set
|
|
|
1244
1555
|
const result = await ctx.client.session.children({ path: { id: sessionId } });
|
|
1245
1556
|
for (const child of result.data ?? []) {
|
|
1246
1557
|
ctx.sessionTitleService.setSessionInfo(child);
|
|
1558
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1559
|
+
registryEntryFromSession(child, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(child.id))
|
|
1560
|
+
);
|
|
1247
1561
|
await hydrateDescendants(child.id, ctx, seen);
|
|
1248
1562
|
}
|
|
1249
1563
|
} catch (err) {
|
|
@@ -1313,6 +1627,7 @@ async function handleSessionIdle(event, ctx) {
|
|
|
1313
1627
|
const sessionId = event.properties.sessionID;
|
|
1314
1628
|
const parentID = await resolveParentID(sessionId, ctx);
|
|
1315
1629
|
ctx.sessionTitleService.setSessionStatus(sessionId, "idle");
|
|
1630
|
+
await ctx.sessionRegistry.updateSession(sessionId, { status: "idle", updatedAt: Date.now() });
|
|
1316
1631
|
if (typeof parentID === "string") {
|
|
1317
1632
|
ctx.logger.info("suppressing child session idle notification", { sessionId, parentID });
|
|
1318
1633
|
await flushDeferredParentIfReady(parentID, ctx);
|
|
@@ -1339,9 +1654,14 @@ async function handleSessionIdle(event, ctx) {
|
|
|
1339
1654
|
async function handleSessionStatus(event, ctx) {
|
|
1340
1655
|
const sessionId = event.properties.sessionID;
|
|
1341
1656
|
const statusType = event.properties.status.type;
|
|
1657
|
+
const previousStatus = ctx.sessionTitleService.getSessionStatus(sessionId);
|
|
1342
1658
|
ctx.sessionTitleService.setSessionStatus(sessionId, statusType);
|
|
1343
1659
|
if (statusType === "idle") {
|
|
1344
1660
|
await handleSessionIdle(event, ctx);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (previousStatus !== statusType) {
|
|
1664
|
+
await ctx.sessionRegistry.updateSession(sessionId, { status: statusType, updatedAt: Date.now() });
|
|
1345
1665
|
}
|
|
1346
1666
|
}
|
|
1347
1667
|
|
|
@@ -1349,6 +1669,9 @@ async function handleSessionStatus(event, ctx) {
|
|
|
1349
1669
|
async function handleSessionUpdated(event, ctx) {
|
|
1350
1670
|
const info = event.properties.info;
|
|
1351
1671
|
ctx.sessionTitleService.setSessionInfo(info);
|
|
1672
|
+
await ctx.sessionRegistry.upsertSession(
|
|
1673
|
+
registryEntryFromSession(info, ctx.serverUrl.href, ctx.sessionTitleService.getSessionStatus(info.id))
|
|
1674
|
+
);
|
|
1352
1675
|
}
|
|
1353
1676
|
|
|
1354
1677
|
// src/lib/html-escape.ts
|
|
@@ -1370,11 +1693,110 @@ function stripCodeFences(input) {
|
|
|
1370
1693
|
var MAX_BODY_CHARS = 3900;
|
|
1371
1694
|
var MAX_TITLE_CHARS = 55;
|
|
1372
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
|
+
}
|
|
1373
1745
|
function createSessionsDispatcher(deps) {
|
|
1374
1746
|
return async ({ chatId, bot }) => {
|
|
1375
|
-
|
|
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
|
+
}
|
|
1376
1798
|
if (sessions.length === 0) {
|
|
1377
|
-
await bot.sendMessage("\
|
|
1799
|
+
await bot.sendMessage("\uC138\uC158\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.", { parse_mode: "HTML" });
|
|
1378
1800
|
return;
|
|
1379
1801
|
}
|
|
1380
1802
|
const capturedAt = Date.now();
|
|
@@ -1382,11 +1804,11 @@ function createSessionsDispatcher(deps) {
|
|
|
1382
1804
|
const entry = {
|
|
1383
1805
|
index: i + 1,
|
|
1384
1806
|
sessionId: session.sessionId,
|
|
1385
|
-
title: session.title
|
|
1807
|
+
title: session.title,
|
|
1386
1808
|
capturedAt
|
|
1387
1809
|
};
|
|
1388
1810
|
if (session.agent !== void 0) entry.agent = session.agent;
|
|
1389
|
-
|
|
1811
|
+
entry.status = session.status;
|
|
1390
1812
|
if (session.serverUrl !== void 0) entry.serverUrl = session.serverUrl;
|
|
1391
1813
|
return entry;
|
|
1392
1814
|
});
|
|
@@ -1394,14 +1816,14 @@ function createSessionsDispatcher(deps) {
|
|
|
1394
1816
|
const lines = entries.map((entry) => {
|
|
1395
1817
|
const agent = entry.agent ? escapeHtml(entry.agent) : "?";
|
|
1396
1818
|
const title = truncateForTelegram(escapeHtml(entry.title), MAX_TITLE_CHARS);
|
|
1397
|
-
const status = entry.status ?? "
|
|
1398
|
-
return `${entry.index}. [${agent}] ${title}
|
|
1819
|
+
const status = ` \u2014 ${escapeHtml(entry.status ?? "idle")}`;
|
|
1820
|
+
return `${entry.index}. [${agent}] ${title}${status}`;
|
|
1399
1821
|
});
|
|
1400
1822
|
let body = lines.join("\n");
|
|
1401
1823
|
if (body.length > MAX_BODY_CHARS) {
|
|
1402
1824
|
body = body.slice(0, MAX_BODY_CHARS) + "\u2026";
|
|
1403
1825
|
}
|
|
1404
|
-
const text = `<b>\
|
|
1826
|
+
const text = `<b>\uCD5C\uADFC \uC138\uC158 (top ${entries.length})</b>
|
|
1405
1827
|
${body}
|
|
1406
1828
|
|
|
1407
1829
|
<i>/status N \uB610\uB294 /start_work N \uC73C\uB85C \uC870\uC791</i>`;
|
|
@@ -1411,13 +1833,13 @@ ${body}
|
|
|
1411
1833
|
}
|
|
1412
1834
|
|
|
1413
1835
|
// src/lib/plan-readiness.ts
|
|
1414
|
-
import { access, readFile as
|
|
1415
|
-
import { join as
|
|
1836
|
+
import { access, readFile as readFile5, readdir as readdir6, stat as stat2 } from "fs/promises";
|
|
1837
|
+
import { join as join6 } from "path";
|
|
1416
1838
|
async function checkPlanReadiness(args) {
|
|
1417
1839
|
const { projectRoot } = args;
|
|
1418
|
-
const omoDir =
|
|
1419
|
-
const plansDir =
|
|
1420
|
-
const boulderPath =
|
|
1840
|
+
const omoDir = join6(projectRoot, ".omo");
|
|
1841
|
+
const plansDir = join6(omoDir, "plans");
|
|
1842
|
+
const boulderPath = join6(omoDir, "boulder.json");
|
|
1421
1843
|
try {
|
|
1422
1844
|
await access(omoDir);
|
|
1423
1845
|
} catch {
|
|
@@ -1438,7 +1860,7 @@ async function checkPlanReadiness(args) {
|
|
|
1438
1860
|
}
|
|
1439
1861
|
let planFiles = [];
|
|
1440
1862
|
try {
|
|
1441
|
-
const entries = await
|
|
1863
|
+
const entries = await readdir6(plansDir);
|
|
1442
1864
|
planFiles = entries.filter((e) => e.endsWith(".md"));
|
|
1443
1865
|
} catch {
|
|
1444
1866
|
return {
|
|
@@ -1456,14 +1878,14 @@ async function checkPlanReadiness(args) {
|
|
|
1456
1878
|
}
|
|
1457
1879
|
const stats = await Promise.all(
|
|
1458
1880
|
planFiles.map(async (f) => {
|
|
1459
|
-
const full =
|
|
1881
|
+
const full = join6(plansDir, f);
|
|
1460
1882
|
const s = await stat2(full);
|
|
1461
1883
|
return { path: full, name: f, mtime: s.mtime.getTime() };
|
|
1462
1884
|
})
|
|
1463
1885
|
);
|
|
1464
1886
|
stats.sort((a, b) => b.mtime - a.mtime);
|
|
1465
1887
|
const latest = stats[0];
|
|
1466
|
-
const content = await
|
|
1888
|
+
const content = await readFile5(latest.path, "utf8");
|
|
1467
1889
|
const totalMatches = content.match(/^- \[[ xX]\]/gm) ?? [];
|
|
1468
1890
|
const completedMatches = content.match(/^- \[[xX]\]/gm) ?? [];
|
|
1469
1891
|
const total = totalMatches.length;
|
|
@@ -1494,7 +1916,7 @@ async function recheckSessionIdle(client, sessionId) {
|
|
|
1494
1916
|
const result = await client.session.status();
|
|
1495
1917
|
const statuses = result.data ?? {};
|
|
1496
1918
|
const sessionStatus = statuses[sessionId];
|
|
1497
|
-
return sessionStatus?.type === "idle";
|
|
1919
|
+
return (sessionStatus?.type ?? "idle") === "idle";
|
|
1498
1920
|
}
|
|
1499
1921
|
|
|
1500
1922
|
// src/events/status-command.ts
|
|
@@ -1593,25 +2015,65 @@ function createStatusDispatcher(deps) {
|
|
|
1593
2015
|
});
|
|
1594
2016
|
return;
|
|
1595
2017
|
}
|
|
1596
|
-
const
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
})
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
const
|
|
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
|
+
}
|
|
1606
2071
|
if (!session || responseStatus === 404) {
|
|
1607
2072
|
await bot.sendMessage("\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694", {
|
|
1608
2073
|
parse_mode: "HTML"
|
|
1609
2074
|
});
|
|
1610
2075
|
return;
|
|
1611
2076
|
}
|
|
1612
|
-
const statusMap = statusResult.data ?? {};
|
|
1613
|
-
const sessionStatus = statusMap[entry.sessionId]?.type ?? "unknown";
|
|
1614
|
-
const messages = messagesResult.data ?? [];
|
|
1615
2077
|
const projectRoot = resolveProjectRoot(session);
|
|
1616
2078
|
const planReady = await checkPlanReadiness({ projectRoot });
|
|
1617
2079
|
const userSnippet = buildSnippet(findLastByRole(messages, "user"));
|
|
@@ -1641,9 +2103,8 @@ function createStatusDispatcher(deps) {
|
|
|
1641
2103
|
}
|
|
1642
2104
|
|
|
1643
2105
|
// src/events/start-work-command.ts
|
|
1644
|
-
function
|
|
1645
|
-
|
|
1646
|
-
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
2106
|
+
function agentFromSession3(session) {
|
|
2107
|
+
return session.agent;
|
|
1647
2108
|
}
|
|
1648
2109
|
function resolveProjectRoot2(session) {
|
|
1649
2110
|
return session.directory;
|
|
@@ -1695,24 +2156,41 @@ function createStartWorkCommandDispatcher(deps) {
|
|
|
1695
2156
|
return;
|
|
1696
2157
|
}
|
|
1697
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);
|
|
1698
2167
|
let session;
|
|
1699
2168
|
try {
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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;
|
|
1704
2183
|
}
|
|
1705
|
-
session = result.data;
|
|
1706
2184
|
} catch (err) {
|
|
1707
2185
|
if (err instanceof Error && isSessionNotFoundError(err)) {
|
|
1708
2186
|
await sendPlain(bot, "\uC138\uC158\uC774 \uB354 \uC774\uC0C1 \uC874\uC7AC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
1709
2187
|
return;
|
|
1710
2188
|
}
|
|
1711
|
-
await sendPlain(bot,
|
|
2189
|
+
await sendPlain(bot, "\uC138\uC158 \uD655\uC778 \uC2E4\uD328. /sessions \uC7AC\uC2E4\uD589 \uD544\uC694");
|
|
1712
2190
|
deps.logger.error("start-work session lookup failed", { sessionId, error: String(err) });
|
|
1713
2191
|
return;
|
|
1714
2192
|
}
|
|
1715
|
-
const agent = deps.sessionTitleService.getSessionAgent(sessionId) ??
|
|
2193
|
+
const agent = deps.sessionTitleService.getSessionAgent(sessionId) ?? agentFromSession3(session) ?? entry.agent;
|
|
1716
2194
|
if (agent !== "plan") {
|
|
1717
2195
|
await sendPlain(
|
|
1718
2196
|
bot,
|
|
@@ -1720,7 +2198,14 @@ function createStartWorkCommandDispatcher(deps) {
|
|
|
1720
2198
|
);
|
|
1721
2199
|
return;
|
|
1722
2200
|
}
|
|
1723
|
-
|
|
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
|
+
}
|
|
1724
2209
|
if (!idle) {
|
|
1725
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`);
|
|
1726
2211
|
return;
|
|
@@ -1731,15 +2216,14 @@ function createStartWorkCommandDispatcher(deps) {
|
|
|
1731
2216
|
return;
|
|
1732
2217
|
}
|
|
1733
2218
|
try {
|
|
1734
|
-
|
|
1735
|
-
await deps.runSessionCommand(sessionId, "start-work", serverUrl);
|
|
2219
|
+
await deps.runSessionCommand(sessionId, "start-work", sourceServerUrl);
|
|
1736
2220
|
await sendHtml(
|
|
1737
2221
|
bot,
|
|
1738
2222
|
`${index}\uBC88 \uC138\uC158\uC5D0 opencode /start-work \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC804\uC1A1 \uC644\uB8CC. (${escapeHtml(entry.title)})`
|
|
1739
2223
|
);
|
|
1740
2224
|
deps.logger.info("start-work dispatched", { chatId, sessionId, index });
|
|
1741
2225
|
} catch (err) {
|
|
1742
|
-
await sendHtml(bot, "opencode /start-work \uC804\uC1A1 \uC2E4\uD328
|
|
2226
|
+
await sendHtml(bot, "opencode /start-work \uC804\uC1A1 \uC2E4\uD328");
|
|
1743
2227
|
deps.logger.error("start-work dispatch failed", { sessionId, error: String(err) });
|
|
1744
2228
|
}
|
|
1745
2229
|
};
|
|
@@ -1776,14 +2260,14 @@ function createHelpDispatcher(deps) {
|
|
|
1776
2260
|
// src/lib/env-loader.ts
|
|
1777
2261
|
import { existsSync } from "fs";
|
|
1778
2262
|
import { homedir } from "os";
|
|
1779
|
-
import { join as
|
|
2263
|
+
import { join as join7 } from "path";
|
|
1780
2264
|
import dotenv from "dotenv";
|
|
1781
2265
|
function loadPluginEnv(opts) {
|
|
1782
2266
|
const paths = [
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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")
|
|
1787
2271
|
];
|
|
1788
2272
|
const loadedFrom = [];
|
|
1789
2273
|
const values = {};
|
|
@@ -1801,10 +2285,10 @@ function loadPluginEnv(opts) {
|
|
|
1801
2285
|
}
|
|
1802
2286
|
|
|
1803
2287
|
// src/lib/lock.ts
|
|
1804
|
-
import { open as open2, readFile as
|
|
2288
|
+
import { open as open2, readFile as readFile6, stat as stat3, unlink as unlink6 } from "fs/promises";
|
|
1805
2289
|
import { hostname } from "os";
|
|
1806
2290
|
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
1807
|
-
function
|
|
2291
|
+
function hasCode6(err, code) {
|
|
1808
2292
|
return "code" in err && err.code === code;
|
|
1809
2293
|
}
|
|
1810
2294
|
function parseLockData(text) {
|
|
@@ -1823,7 +2307,7 @@ function isPidAlive(pid) {
|
|
|
1823
2307
|
process.kill(pid, 0);
|
|
1824
2308
|
return true;
|
|
1825
2309
|
} catch (err) {
|
|
1826
|
-
if (err instanceof Error &&
|
|
2310
|
+
if (err instanceof Error && hasCode6(err, "ESRCH")) return false;
|
|
1827
2311
|
return true;
|
|
1828
2312
|
}
|
|
1829
2313
|
}
|
|
@@ -1844,7 +2328,7 @@ async function createLock(lockPath, pid) {
|
|
|
1844
2328
|
if (released) return;
|
|
1845
2329
|
released = true;
|
|
1846
2330
|
try {
|
|
1847
|
-
await
|
|
2331
|
+
await unlink6(lockPath);
|
|
1848
2332
|
} catch {
|
|
1849
2333
|
}
|
|
1850
2334
|
}
|
|
@@ -1854,7 +2338,7 @@ async function inspectExisting(lockPath, ttlMs) {
|
|
|
1854
2338
|
let ownerPid;
|
|
1855
2339
|
let dead = false;
|
|
1856
2340
|
try {
|
|
1857
|
-
const text = await
|
|
2341
|
+
const text = await readFile6(lockPath, "utf8");
|
|
1858
2342
|
const data = parseLockData(text);
|
|
1859
2343
|
if (data) {
|
|
1860
2344
|
ownerPid = data.pid;
|
|
@@ -1880,7 +2364,7 @@ async function acquireLock(opts) {
|
|
|
1880
2364
|
try {
|
|
1881
2365
|
return { acquired: true, handle: await createLock(opts.lockPath, pid) };
|
|
1882
2366
|
} catch (err) {
|
|
1883
|
-
if (!(err instanceof Error) || !
|
|
2367
|
+
if (!(err instanceof Error) || !hasCode6(err, "EEXIST")) {
|
|
1884
2368
|
return { acquired: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1885
2369
|
}
|
|
1886
2370
|
const existing = await inspectExisting(opts.lockPath, ttlMs);
|
|
@@ -1888,7 +2372,7 @@ async function acquireLock(opts) {
|
|
|
1888
2372
|
return { acquired: false, reason: existing.reason, ownerPid: existing.ownerPid };
|
|
1889
2373
|
}
|
|
1890
2374
|
try {
|
|
1891
|
-
await
|
|
2375
|
+
await unlink6(opts.lockPath);
|
|
1892
2376
|
} catch {
|
|
1893
2377
|
return { acquired: false, reason: "failed to remove stale lock", ownerPid: existing.ownerPid };
|
|
1894
2378
|
}
|
|
@@ -1968,10 +2452,10 @@ function createLogger(opts = {}) {
|
|
|
1968
2452
|
}
|
|
1969
2453
|
|
|
1970
2454
|
// src/lib/session-snapshot.ts
|
|
1971
|
-
import { chmod, mkdir as
|
|
1972
|
-
import { dirname as dirname4, join as
|
|
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";
|
|
1973
2457
|
var TTL_MS = 60 * 60 * 1e3;
|
|
1974
|
-
function
|
|
2458
|
+
function hasCode7(err, code) {
|
|
1975
2459
|
return err instanceof Error && "code" in err && err.code === code;
|
|
1976
2460
|
}
|
|
1977
2461
|
function isSnapshotFile(value) {
|
|
@@ -1990,11 +2474,14 @@ function isSnapshotFile(value) {
|
|
|
1990
2474
|
if (typeof e.capturedAt !== "number") return false;
|
|
1991
2475
|
if (e.agent !== void 0 && typeof e.agent !== "string") return false;
|
|
1992
2476
|
if (e.status !== void 0 && typeof e.status !== "string") return false;
|
|
1993
|
-
if (e.serverUrl !== void 0
|
|
2477
|
+
if (e.serverUrl !== void 0) {
|
|
2478
|
+
if (typeof e.serverUrl !== "string") return false;
|
|
2479
|
+
if (!normalizeOpenCodeServerUrl(e.serverUrl)) return false;
|
|
2480
|
+
}
|
|
1994
2481
|
}
|
|
1995
2482
|
return true;
|
|
1996
2483
|
}
|
|
1997
|
-
function
|
|
2484
|
+
function normalizeEntry2(entry) {
|
|
1998
2485
|
const out = {
|
|
1999
2486
|
index: entry.index,
|
|
2000
2487
|
sessionId: entry.sessionId,
|
|
@@ -2003,22 +2490,25 @@ function normalizeEntry(entry) {
|
|
|
2003
2490
|
};
|
|
2004
2491
|
if (entry.agent !== void 0) out.agent = entry.agent;
|
|
2005
2492
|
if (entry.status !== void 0) out.status = entry.status;
|
|
2006
|
-
if (entry.serverUrl !== void 0)
|
|
2493
|
+
if (entry.serverUrl !== void 0) {
|
|
2494
|
+
const serverUrl = normalizeOpenCodeServerUrl(entry.serverUrl);
|
|
2495
|
+
if (serverUrl !== void 0) out.serverUrl = serverUrl;
|
|
2496
|
+
}
|
|
2007
2497
|
return out;
|
|
2008
2498
|
}
|
|
2009
2499
|
function createSnapshotStore(opts) {
|
|
2010
2500
|
const { configDir, tokenHash, logger } = opts;
|
|
2011
|
-
const snapshotsDir =
|
|
2501
|
+
const snapshotsDir = join8(configDir, "snapshots");
|
|
2012
2502
|
const writeLocks = /* @__PURE__ */ new Map();
|
|
2013
2503
|
function snapshotFilePath(chatId) {
|
|
2014
|
-
return
|
|
2504
|
+
return join8(snapshotsDir, `${tokenHash}-${chatId}.json`);
|
|
2015
2505
|
}
|
|
2016
2506
|
async function performSave(chatId, entries) {
|
|
2017
2507
|
const filePath = snapshotFilePath(chatId);
|
|
2018
2508
|
const parent = dirname4(filePath);
|
|
2019
|
-
await
|
|
2509
|
+
await mkdir6(parent, { recursive: true });
|
|
2020
2510
|
try {
|
|
2021
|
-
await
|
|
2511
|
+
await chmod2(parent, 448);
|
|
2022
2512
|
} catch (err) {
|
|
2023
2513
|
logger.error("snapshot: failed to chmod parent dir", {
|
|
2024
2514
|
path: parent,
|
|
@@ -2029,20 +2519,20 @@ function createSnapshotStore(opts) {
|
|
|
2029
2519
|
version: 1,
|
|
2030
2520
|
chatId,
|
|
2031
2521
|
createdAt: Date.now(),
|
|
2032
|
-
entries: entries.map(
|
|
2522
|
+
entries: entries.map(normalizeEntry2)
|
|
2033
2523
|
};
|
|
2034
2524
|
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
2035
|
-
await
|
|
2525
|
+
await writeFile5(tmpPath, JSON.stringify(payload, null, 2), "utf8");
|
|
2036
2526
|
try {
|
|
2037
|
-
await
|
|
2527
|
+
await rename5(tmpPath, filePath);
|
|
2038
2528
|
} catch (err) {
|
|
2039
2529
|
try {
|
|
2040
|
-
await
|
|
2530
|
+
await unlink7(tmpPath);
|
|
2041
2531
|
} catch {
|
|
2042
2532
|
}
|
|
2043
2533
|
throw err;
|
|
2044
2534
|
}
|
|
2045
|
-
await
|
|
2535
|
+
await chmod2(filePath, 384);
|
|
2046
2536
|
}
|
|
2047
2537
|
async function saveSnapshot(chatId, entries) {
|
|
2048
2538
|
const prev = writeLocks.get(chatId) ?? Promise.resolve();
|
|
@@ -2061,9 +2551,9 @@ function createSnapshotStore(opts) {
|
|
|
2061
2551
|
const filePath = snapshotFilePath(chatId);
|
|
2062
2552
|
let text;
|
|
2063
2553
|
try {
|
|
2064
|
-
text = await
|
|
2554
|
+
text = await readFile7(filePath, "utf8");
|
|
2065
2555
|
} catch (err) {
|
|
2066
|
-
if (
|
|
2556
|
+
if (hasCode7(err, "ENOENT")) return null;
|
|
2067
2557
|
logger.error("snapshot: failed to read file", {
|
|
2068
2558
|
path: filePath,
|
|
2069
2559
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2086,9 +2576,9 @@ function createSnapshotStore(opts) {
|
|
|
2086
2576
|
}
|
|
2087
2577
|
if (parsed.createdAt + TTL_MS < Date.now()) {
|
|
2088
2578
|
try {
|
|
2089
|
-
await
|
|
2579
|
+
await unlink7(filePath);
|
|
2090
2580
|
} catch (err) {
|
|
2091
|
-
if (!
|
|
2581
|
+
if (!hasCode7(err, "ENOENT")) {
|
|
2092
2582
|
logger.error("snapshot: failed to unlink expired file", {
|
|
2093
2583
|
path: filePath,
|
|
2094
2584
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -2097,16 +2587,16 @@ function createSnapshotStore(opts) {
|
|
|
2097
2587
|
}
|
|
2098
2588
|
return null;
|
|
2099
2589
|
}
|
|
2100
|
-
return parsed.entries.map(
|
|
2590
|
+
return parsed.entries.map(normalizeEntry2);
|
|
2101
2591
|
}
|
|
2102
2592
|
return { saveSnapshot, loadSnapshot, snapshotFilePath };
|
|
2103
2593
|
}
|
|
2104
2594
|
|
|
2105
2595
|
// src/lib/state-store.ts
|
|
2106
|
-
import { mkdir as
|
|
2596
|
+
import { mkdir as mkdir7, readFile as readFile8, rename as rename6, writeFile as writeFile6 } from "fs/promises";
|
|
2107
2597
|
import { homedir as homedir2 } from "os";
|
|
2108
|
-
import { dirname as dirname5, join as
|
|
2109
|
-
function
|
|
2598
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
2599
|
+
function hasCode8(err, code) {
|
|
2110
2600
|
return "code" in err && err.code === code;
|
|
2111
2601
|
}
|
|
2112
2602
|
function parseState(text) {
|
|
@@ -2118,28 +2608,28 @@ function parseState(text) {
|
|
|
2118
2608
|
return state;
|
|
2119
2609
|
}
|
|
2120
2610
|
function createStateStore(opts = {}) {
|
|
2121
|
-
const filePath = opts.filePath ??
|
|
2611
|
+
const filePath = opts.filePath ?? join9(homedir2(), ".config/opencode/telegram-remote/state.json");
|
|
2122
2612
|
return {
|
|
2123
2613
|
async read() {
|
|
2124
2614
|
try {
|
|
2125
|
-
return parseState(await
|
|
2615
|
+
return parseState(await readFile8(filePath, "utf8"));
|
|
2126
2616
|
} catch (err) {
|
|
2127
|
-
if (err instanceof Error &&
|
|
2617
|
+
if (err instanceof Error && hasCode8(err, "ENOENT")) return {};
|
|
2128
2618
|
throw err;
|
|
2129
2619
|
}
|
|
2130
2620
|
},
|
|
2131
2621
|
async write(patch) {
|
|
2132
2622
|
const existing = await this.read();
|
|
2133
2623
|
const next = { ...existing, ...patch, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2134
|
-
await
|
|
2624
|
+
await mkdir7(dirname5(filePath), { recursive: true });
|
|
2135
2625
|
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
2136
|
-
await
|
|
2626
|
+
await writeFile6(tmpPath, JSON.stringify(next, null, 2), "utf8");
|
|
2137
2627
|
try {
|
|
2138
|
-
await
|
|
2628
|
+
await rename6(tmpPath, filePath);
|
|
2139
2629
|
} catch (err) {
|
|
2140
|
-
if (!(err instanceof Error) || !
|
|
2141
|
-
await
|
|
2142
|
-
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);
|
|
2143
2633
|
}
|
|
2144
2634
|
return next;
|
|
2145
2635
|
}
|
|
@@ -2147,7 +2637,7 @@ function createStateStore(opts = {}) {
|
|
|
2147
2637
|
}
|
|
2148
2638
|
|
|
2149
2639
|
// src/services/session-title-service.ts
|
|
2150
|
-
function
|
|
2640
|
+
function agentFromSession4(info) {
|
|
2151
2641
|
const candidate = info;
|
|
2152
2642
|
return typeof candidate.agent === "string" ? candidate.agent : void 0;
|
|
2153
2643
|
}
|
|
@@ -2158,7 +2648,7 @@ var SessionTitleService = class {
|
|
|
2158
2648
|
this.sessions.set(info.id, {
|
|
2159
2649
|
title: info.title || null,
|
|
2160
2650
|
parentID: info.parentID ?? null,
|
|
2161
|
-
agent:
|
|
2651
|
+
agent: agentFromSession4(info) ?? existing?.agent,
|
|
2162
2652
|
status: existing?.status,
|
|
2163
2653
|
idleNotificationPending: existing?.idleNotificationPending ?? false,
|
|
2164
2654
|
lastSeenAt: Date.now(),
|
|
@@ -2286,17 +2776,17 @@ var SessionTitleService = class {
|
|
|
2286
2776
|
// src/telegram-remote.ts
|
|
2287
2777
|
var pluginDir = dirname6(fileURLToPath(import.meta.url));
|
|
2288
2778
|
async function postToServer(serverUrl, path, body) {
|
|
2289
|
-
const
|
|
2779
|
+
const safeServerUrl = normalizeOpenCodeServerUrl(serverUrl);
|
|
2780
|
+
if (!safeServerUrl) throw new Error("Invalid OpenCode server URL");
|
|
2781
|
+
const url = new URL(path, safeServerUrl);
|
|
2290
2782
|
const response = await fetch(url, {
|
|
2291
2783
|
method: "POST",
|
|
2292
2784
|
headers: { "Content-Type": "application/json" },
|
|
2293
|
-
body: JSON.stringify(body)
|
|
2785
|
+
body: JSON.stringify(body),
|
|
2786
|
+
redirect: "error"
|
|
2294
2787
|
});
|
|
2295
2788
|
if (response.ok) return;
|
|
2296
|
-
|
|
2297
|
-
throw new Error(
|
|
2298
|
-
`OpenCode request failed: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`
|
|
2299
|
-
);
|
|
2789
|
+
throw new Error(`OpenCode request failed: ${response.status} ${response.statusText}`);
|
|
2300
2790
|
}
|
|
2301
2791
|
function getSessionAgentFromMessage(event) {
|
|
2302
2792
|
const info = event.properties.info;
|
|
@@ -2320,10 +2810,11 @@ var TelegramRemote = async (input) => {
|
|
|
2320
2810
|
const stateStore = createStateStore();
|
|
2321
2811
|
const initialState = await stateStore.read();
|
|
2322
2812
|
const tokenHash = createHash5("sha256").update(config.botToken).digest("hex").slice(0, 16);
|
|
2323
|
-
const configDir =
|
|
2813
|
+
const configDir = join10(homedir3(), ".config/opencode/telegram-remote");
|
|
2324
2814
|
const snapshotStore = createSnapshotStore({ configDir, tokenHash, logger });
|
|
2325
|
-
const
|
|
2326
|
-
const
|
|
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}`);
|
|
2327
2818
|
const pendingQuestions = createPendingQuestionStore({ tokenHash });
|
|
2328
2819
|
const pendingPermissions = createPendingPermissionStore({ tokenHash });
|
|
2329
2820
|
const pendingStartWorks = createPendingStartWorkStore({ tokenHash });
|
|
@@ -2353,8 +2844,8 @@ var TelegramRemote = async (input) => {
|
|
|
2353
2844
|
throwOnError: true
|
|
2354
2845
|
});
|
|
2355
2846
|
};
|
|
2356
|
-
const replyToPermission = async (requestID, sessionID, reply,
|
|
2357
|
-
if (
|
|
2847
|
+
const replyToPermission = async (requestID, sessionID, reply, endpoint2, serverUrl = input.serverUrl.href) => {
|
|
2848
|
+
if (endpoint2 === "request") {
|
|
2358
2849
|
const path2 = `/permission/${encodeURIComponent(requestID)}/reply`;
|
|
2359
2850
|
if (serverUrl !== input.serverUrl.href) {
|
|
2360
2851
|
await postToServer(serverUrl, path2, { reply });
|
|
@@ -2438,6 +2929,7 @@ var TelegramRemote = async (input) => {
|
|
|
2438
2929
|
pendingQuestions,
|
|
2439
2930
|
pendingPermissions,
|
|
2440
2931
|
pendingStartWorks,
|
|
2932
|
+
sessionRegistry,
|
|
2441
2933
|
replyToQuestion,
|
|
2442
2934
|
replyToPermission,
|
|
2443
2935
|
runSessionCommand
|
|
@@ -2446,26 +2938,30 @@ var TelegramRemote = async (input) => {
|
|
|
2446
2938
|
bot.setQuestionDispatcher(createQuestionDispatcher(ctx));
|
|
2447
2939
|
bot.setPermissionDispatcher(createPermissionDispatcher(ctx));
|
|
2448
2940
|
bot.setStartWorkDispatcher(createStartWorkDispatcher(ctx));
|
|
2449
|
-
bot.setSessionsDispatcher(createSessionsDispatcher({
|
|
2450
|
-
|
|
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
|
+
}));
|
|
2451
2956
|
bot.setStartWorkCommandDispatcher(createStartWorkCommandDispatcher({
|
|
2452
2957
|
snapshotStore,
|
|
2453
2958
|
sessionTitleService,
|
|
2454
2959
|
client: input.client,
|
|
2960
|
+
serverUrl: input.serverUrl.href,
|
|
2455
2961
|
runSessionCommand,
|
|
2456
2962
|
logger
|
|
2457
2963
|
}));
|
|
2458
2964
|
bot.setHelpDispatcher(createHelpDispatcher({ logger }));
|
|
2459
|
-
try {
|
|
2460
|
-
const sessions = await input.client.session.list();
|
|
2461
|
-
for (const s of sessions.data ?? []) {
|
|
2462
|
-
sessionTitleService.setSessionInfo(s);
|
|
2463
|
-
sessionTitleService.setServerUrl(s.id, input.serverUrl.href);
|
|
2464
|
-
}
|
|
2465
|
-
logger.info("cold-start cache primed", { count: (sessions.data ?? []).length });
|
|
2466
|
-
} catch (err) {
|
|
2467
|
-
logger.error("cold-start priming failed", { error: String(err) });
|
|
2468
|
-
}
|
|
2469
2965
|
}
|
|
2470
2966
|
return {
|
|
2471
2967
|
event: async ({ event }) => {
|
|
@@ -2485,8 +2981,16 @@ var TelegramRemote = async (input) => {
|
|
|
2485
2981
|
case "message.updated": {
|
|
2486
2982
|
const messageAgent = getSessionAgentFromMessage(event);
|
|
2487
2983
|
if (messageAgent) {
|
|
2984
|
+
const previousAgent = ctx.sessionTitleService.getSessionAgent(messageAgent.sessionID);
|
|
2488
2985
|
ctx.sessionTitleService.setSessionAgent(messageAgent.sessionID, messageAgent.agent);
|
|
2489
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
|
+
}
|
|
2490
2994
|
}
|
|
2491
2995
|
return;
|
|
2492
2996
|
}
|
|
@@ -2495,7 +2999,16 @@ var TelegramRemote = async (input) => {
|
|
|
2495
2999
|
default: {
|
|
2496
3000
|
const stepAgent = getSessionAgentFromNextStep(extEvent);
|
|
2497
3001
|
if (stepAgent) {
|
|
3002
|
+
const previousAgent = ctx.sessionTitleService.getSessionAgent(stepAgent.sessionID);
|
|
2498
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
|
+
}
|
|
2499
3012
|
return;
|
|
2500
3013
|
}
|
|
2501
3014
|
if (isEventPermissionAsked(extEvent)) {
|
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.14",
|
|
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",
|