opencode-telegram-group-topics-bot 0.11.2
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/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dist/agent/manager.js +60 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +47 -0
- package/dist/bot/commands/abort.js +116 -0
- package/dist/bot/commands/commands.js +389 -0
- package/dist/bot/commands/constants.js +20 -0
- package/dist/bot/commands/definitions.js +25 -0
- package/dist/bot/commands/help.js +27 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +247 -0
- package/dist/bot/commands/opencode-start.js +85 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +304 -0
- package/dist/bot/commands/rename.js +173 -0
- package/dist/bot/commands/sessions.js +491 -0
- package/dist/bot/commands/start.js +67 -0
- package/dist/bot/commands/status.js +138 -0
- package/dist/bot/constants.js +49 -0
- package/dist/bot/handlers/agent.js +127 -0
- package/dist/bot/handlers/context.js +125 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +124 -0
- package/dist/bot/handlers/model.js +152 -0
- package/dist/bot/handlers/permission.js +281 -0
- package/dist/bot/handlers/prompt.js +263 -0
- package/dist/bot/handlers/question.js +285 -0
- package/dist/bot/handlers/variant.js +147 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +945 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/scope.js +222 -0
- package/dist/bot/telegram-constants.js +3 -0
- package/dist/bot/telegram-rate-limiter.js +263 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/keyboard.js +85 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +57 -0
- package/dist/bot/utils/session-error-filter.js +34 -0
- package/dist/bot/utils/topic-link.js +29 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +103 -0
- package/dist/i18n/de.js +330 -0
- package/dist/i18n/en.js +330 -0
- package/dist/i18n/es.js +330 -0
- package/dist/i18n/index.js +102 -0
- package/dist/i18n/ru.js +330 -0
- package/dist/i18n/zh.js +330 -0
- package/dist/index.js +28 -0
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/constants.js +25 -0
- package/dist/interaction/guard.js +100 -0
- package/dist/interaction/manager.js +113 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +115 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/manager.js +257 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +159 -0
- package/dist/opencode/prompt-submit-error.js +101 -0
- package/dist/permission/manager.js +92 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/manager.js +405 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +186 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +64 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/runtime/process-error-handlers.js +24 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +87 -0
- package/dist/settings/manager.js +283 -0
- package/dist/stt/client.js +64 -0
- package/dist/summary/aggregator.js +625 -0
- package/dist/summary/formatter.js +417 -0
- package/dist/summary/tool-message-batcher.js +277 -0
- package/dist/topic/colors.js +8 -0
- package/dist/topic/constants.js +10 -0
- package/dist/topic/manager.js +161 -0
- package/dist/topic/title-constants.js +2 -0
- package/dist/topic/title-format.js +10 -0
- package/dist/topic/title-sync.js +17 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +175 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import dotenv from "dotenv";
|
|
7
|
+
import { getRuntimePaths } from "./paths.js";
|
|
8
|
+
import { getLocale, getLocaleOptions, resolveSupportedLocale, setRuntimeLocale, t, } from "../i18n/index.js";
|
|
9
|
+
const DEFAULT_API_URL = "http://localhost:4096";
|
|
10
|
+
const DEFAULT_SERVER_USERNAME = "opencode";
|
|
11
|
+
const FALLBACK_MODEL_PROVIDER = "opencode";
|
|
12
|
+
const FALLBACK_MODEL_ID = "big-pickle";
|
|
13
|
+
function isPositiveInteger(value) {
|
|
14
|
+
return /^[1-9]\d*$/.test(value);
|
|
15
|
+
}
|
|
16
|
+
function isValidHttpUrl(value) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(value);
|
|
19
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function validateRuntimeEnvValues(values) {
|
|
26
|
+
if (!values.TELEGRAM_BOT_TOKEN || values.TELEGRAM_BOT_TOKEN.trim().length === 0) {
|
|
27
|
+
return { isValid: false, reason: "Missing TELEGRAM_BOT_TOKEN" };
|
|
28
|
+
}
|
|
29
|
+
if (!isPositiveInteger(values.TELEGRAM_ALLOWED_USER_ID || "")) {
|
|
30
|
+
return { isValid: false, reason: "Invalid TELEGRAM_ALLOWED_USER_ID" };
|
|
31
|
+
}
|
|
32
|
+
if (!values.OPENCODE_MODEL_PROVIDER || values.OPENCODE_MODEL_PROVIDER.trim().length === 0) {
|
|
33
|
+
return { isValid: false, reason: "Missing OPENCODE_MODEL_PROVIDER" };
|
|
34
|
+
}
|
|
35
|
+
if (!values.OPENCODE_MODEL_ID || values.OPENCODE_MODEL_ID.trim().length === 0) {
|
|
36
|
+
return { isValid: false, reason: "Missing OPENCODE_MODEL_ID" };
|
|
37
|
+
}
|
|
38
|
+
const apiUrl = values.OPENCODE_API_URL?.trim();
|
|
39
|
+
if (apiUrl && !isValidHttpUrl(apiUrl)) {
|
|
40
|
+
return { isValid: false, reason: "Invalid OPENCODE_API_URL" };
|
|
41
|
+
}
|
|
42
|
+
return { isValid: true };
|
|
43
|
+
}
|
|
44
|
+
function escapeRegex(value) {
|
|
45
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
46
|
+
}
|
|
47
|
+
function normalizeEnvLineEndings(content) {
|
|
48
|
+
const lines = content.split(/\r?\n/).map((line) => line.replace(/\r$/, ""));
|
|
49
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
50
|
+
lines.pop();
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
function removeEnvKey(lines, key) {
|
|
55
|
+
const regex = new RegExp(`^\\s*(?:export\\s+)?${escapeRegex(key)}\\s*=`);
|
|
56
|
+
return lines.filter((line) => !regex.test(line));
|
|
57
|
+
}
|
|
58
|
+
function finalizeEnvContent(lines) {
|
|
59
|
+
return `${lines.join("\n")}\n`;
|
|
60
|
+
}
|
|
61
|
+
export function buildEnvFileContent(existingContent, values) {
|
|
62
|
+
let lines = normalizeEnvLineEndings(existingContent);
|
|
63
|
+
const orderedUpdates = [
|
|
64
|
+
["BOT_LOCALE", values.BOT_LOCALE],
|
|
65
|
+
["TELEGRAM_BOT_TOKEN", values.TELEGRAM_BOT_TOKEN],
|
|
66
|
+
["TELEGRAM_ALLOWED_USER_ID", values.TELEGRAM_ALLOWED_USER_ID],
|
|
67
|
+
["OPENCODE_API_URL", values.OPENCODE_API_URL],
|
|
68
|
+
["OPENCODE_SERVER_USERNAME", values.OPENCODE_SERVER_USERNAME],
|
|
69
|
+
["OPENCODE_SERVER_PASSWORD", values.OPENCODE_SERVER_PASSWORD],
|
|
70
|
+
["OPENCODE_MODEL_PROVIDER", values.OPENCODE_MODEL_PROVIDER],
|
|
71
|
+
["OPENCODE_MODEL_ID", values.OPENCODE_MODEL_ID],
|
|
72
|
+
];
|
|
73
|
+
for (const [key, value] of orderedUpdates) {
|
|
74
|
+
lines = removeEnvKey(lines, key);
|
|
75
|
+
if (value && value.trim().length > 0) {
|
|
76
|
+
lines.push(`${key}=${value}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return finalizeEnvContent(lines);
|
|
80
|
+
}
|
|
81
|
+
async function readEnvFileIfExists(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
return await fs.readFile(filePath, "utf-8");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code === "ENOENT") {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function writeFileAtomically(filePath, content) {
|
|
93
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
94
|
+
const tempFilePath = `${filePath}.${process.pid}.tmp`;
|
|
95
|
+
await fs.writeFile(tempFilePath, content, "utf-8");
|
|
96
|
+
await fs.rename(tempFilePath, filePath);
|
|
97
|
+
}
|
|
98
|
+
async function ensureSettingsFile(settingsFilePath) {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(settingsFilePath);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== "ENOENT") {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await fs.mkdir(path.dirname(settingsFilePath), { recursive: true });
|
|
109
|
+
await fs.writeFile(settingsFilePath, "{}\n", "utf-8");
|
|
110
|
+
}
|
|
111
|
+
function getEnvExamplePath() {
|
|
112
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
113
|
+
return path.resolve(path.dirname(currentFilePath), "..", "..", ".env.example");
|
|
114
|
+
}
|
|
115
|
+
async function loadModelDefaultsFromEnvExample() {
|
|
116
|
+
const fallbackDefaults = {
|
|
117
|
+
provider: FALLBACK_MODEL_PROVIDER,
|
|
118
|
+
modelId: FALLBACK_MODEL_ID,
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
const content = await fs.readFile(getEnvExamplePath(), "utf-8");
|
|
122
|
+
const parsed = dotenv.parse(content);
|
|
123
|
+
const provider = parsed.OPENCODE_MODEL_PROVIDER?.trim();
|
|
124
|
+
const modelId = parsed.OPENCODE_MODEL_ID?.trim();
|
|
125
|
+
if (!provider || !modelId) {
|
|
126
|
+
return fallbackDefaults;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
provider,
|
|
130
|
+
modelId,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return fallbackDefaults;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function askVisible(question) {
|
|
138
|
+
const rl = createInterface({
|
|
139
|
+
input: process.stdin,
|
|
140
|
+
output: process.stdout,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
const answer = await rl.question(question);
|
|
144
|
+
return answer.trim();
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function askHidden(question) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const rl = readline.createInterface({
|
|
153
|
+
input: process.stdin,
|
|
154
|
+
output: process.stdout,
|
|
155
|
+
terminal: true,
|
|
156
|
+
});
|
|
157
|
+
const maskedRl = rl;
|
|
158
|
+
maskedRl._writeToOutput = (value) => {
|
|
159
|
+
if (maskedRl.stdoutMuted) {
|
|
160
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
161
|
+
process.stdout.write(value);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (value.length > 0) {
|
|
165
|
+
process.stdout.write("*");
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
process.stdout.write(value);
|
|
170
|
+
};
|
|
171
|
+
maskedRl.stdoutMuted = false;
|
|
172
|
+
rl.question(question, (answer) => {
|
|
173
|
+
maskedRl.stdoutMuted = false;
|
|
174
|
+
process.stdout.write("\n");
|
|
175
|
+
rl.close();
|
|
176
|
+
resolve(answer.trim());
|
|
177
|
+
});
|
|
178
|
+
maskedRl.stdoutMuted = true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async function askToken() {
|
|
182
|
+
for (;;) {
|
|
183
|
+
const token = await askHidden(t("runtime.wizard.ask_token"));
|
|
184
|
+
if (!token) {
|
|
185
|
+
process.stdout.write(t("runtime.wizard.token_required"));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!token.includes(":")) {
|
|
189
|
+
process.stdout.write(t("runtime.wizard.token_invalid"));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
return token;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function askLocale() {
|
|
196
|
+
const localeOptions = getLocaleOptions();
|
|
197
|
+
const defaultLocale = getLocale();
|
|
198
|
+
const defaultLocaleOption = localeOptions.find((localeOption) => localeOption.code === defaultLocale) ?? localeOptions[0];
|
|
199
|
+
const optionsText = localeOptions
|
|
200
|
+
.map((localeOption, index) => `${index + 1} - ${localeOption.label} (${localeOption.code})`)
|
|
201
|
+
.join("\n");
|
|
202
|
+
const prompt = t("runtime.wizard.ask_language", {
|
|
203
|
+
options: optionsText,
|
|
204
|
+
defaultLocale: `${defaultLocaleOption.label} (${defaultLocaleOption.code})`,
|
|
205
|
+
});
|
|
206
|
+
for (;;) {
|
|
207
|
+
const answer = await askVisible(prompt);
|
|
208
|
+
if (!answer) {
|
|
209
|
+
return defaultLocaleOption.code;
|
|
210
|
+
}
|
|
211
|
+
if (/^\d+$/.test(answer)) {
|
|
212
|
+
const index = Number.parseInt(answer, 10) - 1;
|
|
213
|
+
if (index >= 0 && index < localeOptions.length) {
|
|
214
|
+
return localeOptions[index].code;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const localeByCode = resolveSupportedLocale(answer);
|
|
218
|
+
if (localeByCode) {
|
|
219
|
+
return localeByCode;
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(t("runtime.wizard.language_invalid"));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function askAllowedUserId() {
|
|
225
|
+
for (;;) {
|
|
226
|
+
const allowedUserId = await askVisible(t("runtime.wizard.ask_user_id"));
|
|
227
|
+
if (!isPositiveInteger(allowedUserId)) {
|
|
228
|
+
process.stdout.write(t("runtime.wizard.user_id_invalid"));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
return allowedUserId;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function askApiUrl() {
|
|
235
|
+
const prompt = t("runtime.wizard.ask_api_url", { defaultUrl: DEFAULT_API_URL });
|
|
236
|
+
for (;;) {
|
|
237
|
+
const apiUrl = await askVisible(prompt);
|
|
238
|
+
if (!apiUrl) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
if (!isValidHttpUrl(apiUrl)) {
|
|
242
|
+
process.stdout.write(t("runtime.wizard.api_url_invalid"));
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
return apiUrl;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function askServerUsername() {
|
|
249
|
+
const prompt = t("runtime.wizard.ask_server_username", {
|
|
250
|
+
defaultUsername: DEFAULT_SERVER_USERNAME,
|
|
251
|
+
});
|
|
252
|
+
const username = await askVisible(prompt);
|
|
253
|
+
if (!username) {
|
|
254
|
+
return DEFAULT_SERVER_USERNAME;
|
|
255
|
+
}
|
|
256
|
+
return username;
|
|
257
|
+
}
|
|
258
|
+
async function askServerPassword() {
|
|
259
|
+
const password = await askHidden(t("runtime.wizard.ask_server_password"));
|
|
260
|
+
if (!password) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return password;
|
|
264
|
+
}
|
|
265
|
+
async function collectWizardValues() {
|
|
266
|
+
const locale = await askLocale();
|
|
267
|
+
setRuntimeLocale(locale);
|
|
268
|
+
const selectedLocaleOption = getLocaleOptions().find((localeOption) => localeOption.code === locale) ?? null;
|
|
269
|
+
process.stdout.write("\n");
|
|
270
|
+
process.stdout.write(t("runtime.wizard.language_selected", {
|
|
271
|
+
language: selectedLocaleOption !== null
|
|
272
|
+
? `${selectedLocaleOption.label} (${selectedLocaleOption.code})`
|
|
273
|
+
: locale,
|
|
274
|
+
}));
|
|
275
|
+
process.stdout.write("\n");
|
|
276
|
+
process.stdout.write(t("runtime.wizard.start"));
|
|
277
|
+
process.stdout.write("\n");
|
|
278
|
+
const token = await askToken();
|
|
279
|
+
const allowedUserId = await askAllowedUserId();
|
|
280
|
+
const apiUrl = await askApiUrl();
|
|
281
|
+
const serverUsername = await askServerUsername();
|
|
282
|
+
const serverPassword = await askServerPassword();
|
|
283
|
+
process.stdout.write("\n");
|
|
284
|
+
return {
|
|
285
|
+
locale,
|
|
286
|
+
token,
|
|
287
|
+
allowedUserId,
|
|
288
|
+
apiUrl,
|
|
289
|
+
serverUsername,
|
|
290
|
+
serverPassword,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function ensureInteractiveTty() {
|
|
294
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
295
|
+
throw new Error(t("runtime.wizard.tty_required"));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function validateExistingEnv(envFilePath) {
|
|
299
|
+
const content = await readEnvFileIfExists(envFilePath);
|
|
300
|
+
if (content === null) {
|
|
301
|
+
return { isValid: false, reason: "Missing .env" };
|
|
302
|
+
}
|
|
303
|
+
const parsed = dotenv.parse(content);
|
|
304
|
+
return validateRuntimeEnvValues(parsed);
|
|
305
|
+
}
|
|
306
|
+
async function runWizardAndPersist(runtimePaths) {
|
|
307
|
+
ensureInteractiveTty();
|
|
308
|
+
const [existingContent, modelDefaults, wizardValues] = await Promise.all([
|
|
309
|
+
readEnvFileIfExists(runtimePaths.envFilePath),
|
|
310
|
+
loadModelDefaultsFromEnvExample(),
|
|
311
|
+
collectWizardValues(),
|
|
312
|
+
]);
|
|
313
|
+
const existingParsed = existingContent ? dotenv.parse(existingContent) : {};
|
|
314
|
+
const provider = existingParsed.OPENCODE_MODEL_PROVIDER || modelDefaults.provider;
|
|
315
|
+
const modelId = existingParsed.OPENCODE_MODEL_ID || modelDefaults.modelId;
|
|
316
|
+
const envValues = {
|
|
317
|
+
BOT_LOCALE: wizardValues.locale,
|
|
318
|
+
TELEGRAM_BOT_TOKEN: wizardValues.token,
|
|
319
|
+
TELEGRAM_ALLOWED_USER_ID: wizardValues.allowedUserId,
|
|
320
|
+
OPENCODE_API_URL: wizardValues.apiUrl,
|
|
321
|
+
OPENCODE_SERVER_USERNAME: wizardValues.serverUsername,
|
|
322
|
+
OPENCODE_SERVER_PASSWORD: wizardValues.serverPassword,
|
|
323
|
+
OPENCODE_MODEL_PROVIDER: provider,
|
|
324
|
+
OPENCODE_MODEL_ID: modelId,
|
|
325
|
+
};
|
|
326
|
+
const envContent = buildEnvFileContent(existingContent ?? "", envValues);
|
|
327
|
+
await writeFileAtomically(runtimePaths.envFilePath, envContent);
|
|
328
|
+
await ensureSettingsFile(runtimePaths.settingsFilePath);
|
|
329
|
+
process.stdout.write(t("runtime.wizard.saved", {
|
|
330
|
+
envPath: runtimePaths.envFilePath,
|
|
331
|
+
settingsPath: runtimePaths.settingsFilePath,
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
export async function ensureRuntimeConfigForStart() {
|
|
335
|
+
const runtimePaths = getRuntimePaths();
|
|
336
|
+
if (runtimePaths.mode !== "installed") {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const validationResult = await validateExistingEnv(runtimePaths.envFilePath);
|
|
340
|
+
if (validationResult.isValid) {
|
|
341
|
+
await ensureSettingsFile(runtimePaths.settingsFilePath);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
process.stdout.write(t("runtime.wizard.not_configured_starting"));
|
|
345
|
+
await runWizardAndPersist(runtimePaths);
|
|
346
|
+
}
|
|
347
|
+
export async function runConfigWizardCommand() {
|
|
348
|
+
const runtimePaths = getRuntimePaths();
|
|
349
|
+
await runWizardAndPersist(runtimePaths);
|
|
350
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const RUNTIME_MODE_ENV_KEY = "OPENCODE_TELEGRAM_RUNTIME_MODE";
|
|
2
|
+
function normalizeMode(value) {
|
|
3
|
+
if (value === "installed") {
|
|
4
|
+
return "installed";
|
|
5
|
+
}
|
|
6
|
+
if (value === "sources") {
|
|
7
|
+
return "sources";
|
|
8
|
+
}
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function parseModeFromArgv(argv) {
|
|
12
|
+
let mode = null;
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const token = argv[index];
|
|
15
|
+
if (token === "--mode") {
|
|
16
|
+
const modeValue = argv[index + 1];
|
|
17
|
+
if (!modeValue || modeValue.startsWith("-")) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
mode = normalizeMode(modeValue);
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (token.startsWith("--mode=")) {
|
|
25
|
+
mode = normalizeMode(token.slice("--mode=".length));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return mode;
|
|
29
|
+
}
|
|
30
|
+
function hasInvalidModeSyntax(argv) {
|
|
31
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
32
|
+
const token = argv[index];
|
|
33
|
+
if (token === "--mode") {
|
|
34
|
+
const modeValue = argv[index + 1];
|
|
35
|
+
if (!modeValue || modeValue.startsWith("-")) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (!normalizeMode(modeValue)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (token.startsWith("--mode=")) {
|
|
45
|
+
const modeValue = token.slice("--mode=".length);
|
|
46
|
+
if (!normalizeMode(modeValue)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
export function resolveRuntimeMode(options) {
|
|
54
|
+
if (options.explicitMode) {
|
|
55
|
+
return { mode: options.explicitMode };
|
|
56
|
+
}
|
|
57
|
+
const argv = options.argv ?? [];
|
|
58
|
+
if (hasInvalidModeSyntax(argv)) {
|
|
59
|
+
return {
|
|
60
|
+
mode: options.defaultMode,
|
|
61
|
+
error: "Invalid value for --mode. Expected sources|installed",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const modeFromArgv = parseModeFromArgv(argv);
|
|
65
|
+
return { mode: modeFromArgv ?? options.defaultMode };
|
|
66
|
+
}
|
|
67
|
+
export function setRuntimeMode(mode) {
|
|
68
|
+
process.env[RUNTIME_MODE_ENV_KEY] = mode;
|
|
69
|
+
}
|
|
70
|
+
export function getRuntimeMode() {
|
|
71
|
+
const rawMode = process.env[RUNTIME_MODE_ENV_KEY];
|
|
72
|
+
const normalized = rawMode ? normalizeMode(rawMode) : null;
|
|
73
|
+
return normalized ?? "sources";
|
|
74
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getRuntimeMode } from "./mode.js";
|
|
4
|
+
const APP_DIR_NAME = "opencode-telegram-group-topics-bot";
|
|
5
|
+
function getInstalledAppHome() {
|
|
6
|
+
if (process.platform === "win32") {
|
|
7
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
8
|
+
return path.join(appData, APP_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
if (process.platform === "darwin") {
|
|
11
|
+
return path.join(os.homedir(), "Library", "Application Support", APP_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
14
|
+
return path.join(xdgConfigHome, APP_DIR_NAME);
|
|
15
|
+
}
|
|
16
|
+
function resolveAppHome(mode) {
|
|
17
|
+
const homeOverride = process.env.OPENCODE_TELEGRAM_HOME;
|
|
18
|
+
if (homeOverride && homeOverride.trim().length > 0) {
|
|
19
|
+
return path.resolve(homeOverride);
|
|
20
|
+
}
|
|
21
|
+
if (mode === "sources") {
|
|
22
|
+
return process.cwd();
|
|
23
|
+
}
|
|
24
|
+
return getInstalledAppHome();
|
|
25
|
+
}
|
|
26
|
+
export function getRuntimePaths() {
|
|
27
|
+
const mode = getRuntimeMode();
|
|
28
|
+
const appHome = resolveAppHome(mode);
|
|
29
|
+
return {
|
|
30
|
+
mode,
|
|
31
|
+
appHome,
|
|
32
|
+
envFilePath: path.join(appHome, ".env"),
|
|
33
|
+
settingsFilePath: path.join(appHome, "settings.json"),
|
|
34
|
+
logsDirPath: path.join(appHome, "logs"),
|
|
35
|
+
runDirPath: path.join(appHome, "run"),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
2
|
+
const PROCESS_EVENT = {
|
|
3
|
+
UNHANDLED_REJECTION: "unhandledRejection",
|
|
4
|
+
UNCAUGHT_EXCEPTION: "uncaughtException",
|
|
5
|
+
};
|
|
6
|
+
let installed = false;
|
|
7
|
+
export function installProcessErrorHandlers() {
|
|
8
|
+
if (installed) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
installed = true;
|
|
12
|
+
process.on(PROCESS_EVENT.UNHANDLED_REJECTION, (reason) => {
|
|
13
|
+
logger.error("[Runtime] Unhandled promise rejection", {
|
|
14
|
+
reason,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
process.on(PROCESS_EVENT.UNCAUGHT_EXCEPTION, (error) => {
|
|
18
|
+
logger.error("[Runtime] Uncaught exception", {
|
|
19
|
+
name: error.name,
|
|
20
|
+
message: error.message,
|
|
21
|
+
stack: error.stack,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|