@grinev/opencode-telegram-bot 0.1.0-rc.1

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.
Files changed (66) hide show
  1. package/.env.example +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -0
  4. package/dist/agent/manager.js +92 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +26 -0
  7. package/dist/bot/commands/agent.js +16 -0
  8. package/dist/bot/commands/definitions.js +20 -0
  9. package/dist/bot/commands/help.js +7 -0
  10. package/dist/bot/commands/model.js +16 -0
  11. package/dist/bot/commands/models.js +37 -0
  12. package/dist/bot/commands/new.js +58 -0
  13. package/dist/bot/commands/opencode-start.js +87 -0
  14. package/dist/bot/commands/opencode-stop.js +46 -0
  15. package/dist/bot/commands/projects.js +104 -0
  16. package/dist/bot/commands/server-restart.js +23 -0
  17. package/dist/bot/commands/server-start.js +23 -0
  18. package/dist/bot/commands/sessions.js +240 -0
  19. package/dist/bot/commands/start.js +40 -0
  20. package/dist/bot/commands/status.js +63 -0
  21. package/dist/bot/commands/stop.js +92 -0
  22. package/dist/bot/handlers/agent.js +96 -0
  23. package/dist/bot/handlers/context.js +112 -0
  24. package/dist/bot/handlers/model.js +115 -0
  25. package/dist/bot/handlers/permission.js +158 -0
  26. package/dist/bot/handlers/question.js +294 -0
  27. package/dist/bot/handlers/variant.js +126 -0
  28. package/dist/bot/index.js +573 -0
  29. package/dist/bot/middleware/auth.js +30 -0
  30. package/dist/bot/utils/keyboard.js +66 -0
  31. package/dist/cli/args.js +97 -0
  32. package/dist/cli.js +90 -0
  33. package/dist/config.js +46 -0
  34. package/dist/index.js +26 -0
  35. package/dist/keyboard/manager.js +171 -0
  36. package/dist/keyboard/types.js +1 -0
  37. package/dist/model/manager.js +123 -0
  38. package/dist/model/types.js +26 -0
  39. package/dist/opencode/client.js +13 -0
  40. package/dist/opencode/events.js +79 -0
  41. package/dist/opencode/server.js +104 -0
  42. package/dist/permission/manager.js +78 -0
  43. package/dist/permission/types.js +1 -0
  44. package/dist/pinned/manager.js +610 -0
  45. package/dist/pinned/types.js +1 -0
  46. package/dist/pinned-message/service.js +54 -0
  47. package/dist/process/manager.js +273 -0
  48. package/dist/process/types.js +1 -0
  49. package/dist/project/manager.js +28 -0
  50. package/dist/question/manager.js +143 -0
  51. package/dist/question/types.js +1 -0
  52. package/dist/runtime/bootstrap.js +278 -0
  53. package/dist/runtime/mode.js +74 -0
  54. package/dist/runtime/paths.js +37 -0
  55. package/dist/session/manager.js +10 -0
  56. package/dist/session/state.js +24 -0
  57. package/dist/settings/manager.js +99 -0
  58. package/dist/status/formatter.js +44 -0
  59. package/dist/summary/aggregator.js +427 -0
  60. package/dist/summary/formatter.js +226 -0
  61. package/dist/utils/formatting.js +237 -0
  62. package/dist/utils/logger.js +59 -0
  63. package/dist/utils/safe-background-task.js +33 -0
  64. package/dist/variant/manager.js +103 -0
  65. package/dist/variant/types.js +1 -0
  66. package/package.json +63 -0
@@ -0,0 +1,278 @@
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
+ const DEFAULT_API_URL = "http://localhost:4096";
9
+ const FALLBACK_MODEL_PROVIDER = "opencode";
10
+ const FALLBACK_MODEL_ID = "big-pickle";
11
+ function isPositiveInteger(value) {
12
+ return /^[1-9]\d*$/.test(value);
13
+ }
14
+ function isValidHttpUrl(value) {
15
+ try {
16
+ const parsed = new URL(value);
17
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function validateRuntimeEnvValues(values) {
24
+ if (!values.TELEGRAM_BOT_TOKEN || values.TELEGRAM_BOT_TOKEN.trim().length === 0) {
25
+ return { isValid: false, reason: "Missing TELEGRAM_BOT_TOKEN" };
26
+ }
27
+ if (!isPositiveInteger(values.TELEGRAM_ALLOWED_USER_ID || "")) {
28
+ return { isValid: false, reason: "Invalid TELEGRAM_ALLOWED_USER_ID" };
29
+ }
30
+ if (!values.OPENCODE_MODEL_PROVIDER || values.OPENCODE_MODEL_PROVIDER.trim().length === 0) {
31
+ return { isValid: false, reason: "Missing OPENCODE_MODEL_PROVIDER" };
32
+ }
33
+ if (!values.OPENCODE_MODEL_ID || values.OPENCODE_MODEL_ID.trim().length === 0) {
34
+ return { isValid: false, reason: "Missing OPENCODE_MODEL_ID" };
35
+ }
36
+ const apiUrl = values.OPENCODE_API_URL?.trim();
37
+ if (apiUrl && !isValidHttpUrl(apiUrl)) {
38
+ return { isValid: false, reason: "Invalid OPENCODE_API_URL" };
39
+ }
40
+ return { isValid: true };
41
+ }
42
+ function escapeRegex(value) {
43
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
44
+ }
45
+ function normalizeEnvLineEndings(content) {
46
+ const lines = content.split(/\r?\n/).map((line) => line.replace(/\r$/, ""));
47
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
48
+ lines.pop();
49
+ }
50
+ return lines;
51
+ }
52
+ function removeEnvKey(lines, key) {
53
+ const regex = new RegExp(`^\\s*(?:export\\s+)?${escapeRegex(key)}\\s*=`);
54
+ return lines.filter((line) => !regex.test(line));
55
+ }
56
+ function finalizeEnvContent(lines) {
57
+ return `${lines.join("\n")}\n`;
58
+ }
59
+ export function buildEnvFileContent(existingContent, values) {
60
+ let lines = normalizeEnvLineEndings(existingContent);
61
+ const orderedUpdates = [
62
+ ["TELEGRAM_BOT_TOKEN", values.TELEGRAM_BOT_TOKEN],
63
+ ["TELEGRAM_ALLOWED_USER_ID", values.TELEGRAM_ALLOWED_USER_ID],
64
+ ["OPENCODE_API_URL", values.OPENCODE_API_URL],
65
+ ["OPENCODE_MODEL_PROVIDER", values.OPENCODE_MODEL_PROVIDER],
66
+ ["OPENCODE_MODEL_ID", values.OPENCODE_MODEL_ID],
67
+ ];
68
+ for (const [key, value] of orderedUpdates) {
69
+ lines = removeEnvKey(lines, key);
70
+ if (value && value.trim().length > 0) {
71
+ lines.push(`${key}=${value}`);
72
+ }
73
+ }
74
+ return finalizeEnvContent(lines);
75
+ }
76
+ async function readEnvFileIfExists(filePath) {
77
+ try {
78
+ return await fs.readFile(filePath, "utf-8");
79
+ }
80
+ catch (error) {
81
+ if (error.code === "ENOENT") {
82
+ return null;
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ async function writeFileAtomically(filePath, content) {
88
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
89
+ const tempFilePath = `${filePath}.${process.pid}.tmp`;
90
+ await fs.writeFile(tempFilePath, content, "utf-8");
91
+ await fs.rename(tempFilePath, filePath);
92
+ }
93
+ async function ensureSettingsFile(settingsFilePath) {
94
+ try {
95
+ await fs.access(settingsFilePath);
96
+ return;
97
+ }
98
+ catch (error) {
99
+ if (error.code !== "ENOENT") {
100
+ throw error;
101
+ }
102
+ }
103
+ await fs.mkdir(path.dirname(settingsFilePath), { recursive: true });
104
+ await fs.writeFile(settingsFilePath, "{}\n", "utf-8");
105
+ }
106
+ function getEnvExamplePath() {
107
+ const currentFilePath = fileURLToPath(import.meta.url);
108
+ return path.resolve(path.dirname(currentFilePath), "..", "..", ".env.example");
109
+ }
110
+ async function loadModelDefaultsFromEnvExample() {
111
+ const fallbackDefaults = {
112
+ provider: FALLBACK_MODEL_PROVIDER,
113
+ modelId: FALLBACK_MODEL_ID,
114
+ };
115
+ try {
116
+ const content = await fs.readFile(getEnvExamplePath(), "utf-8");
117
+ const parsed = dotenv.parse(content);
118
+ const provider = parsed.OPENCODE_MODEL_PROVIDER?.trim();
119
+ const modelId = parsed.OPENCODE_MODEL_ID?.trim();
120
+ if (!provider || !modelId) {
121
+ return fallbackDefaults;
122
+ }
123
+ return {
124
+ provider,
125
+ modelId,
126
+ };
127
+ }
128
+ catch {
129
+ return fallbackDefaults;
130
+ }
131
+ }
132
+ async function askVisible(question) {
133
+ const rl = createInterface({
134
+ input: process.stdin,
135
+ output: process.stdout,
136
+ });
137
+ try {
138
+ const answer = await rl.question(question);
139
+ return answer.trim();
140
+ }
141
+ finally {
142
+ rl.close();
143
+ }
144
+ }
145
+ async function askHidden(question) {
146
+ return new Promise((resolve) => {
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout,
150
+ terminal: true,
151
+ });
152
+ const maskedRl = rl;
153
+ maskedRl._writeToOutput = (value) => {
154
+ if (maskedRl.stdoutMuted) {
155
+ if (value.includes("\n") || value.includes("\r")) {
156
+ process.stdout.write(value);
157
+ return;
158
+ }
159
+ if (value.length > 0) {
160
+ process.stdout.write("*");
161
+ }
162
+ return;
163
+ }
164
+ process.stdout.write(value);
165
+ };
166
+ maskedRl.stdoutMuted = false;
167
+ rl.question(question, (answer) => {
168
+ maskedRl.stdoutMuted = false;
169
+ process.stdout.write("\n");
170
+ rl.close();
171
+ resolve(answer.trim());
172
+ });
173
+ maskedRl.stdoutMuted = true;
174
+ });
175
+ }
176
+ async function askToken() {
177
+ for (;;) {
178
+ const token = await askHidden("Введите токен Telegram-бота (получить у @BotFather).\n> ");
179
+ if (!token) {
180
+ process.stdout.write("Токен обязателен. Попробуйте еще раз.\n");
181
+ continue;
182
+ }
183
+ if (!token.includes(":")) {
184
+ process.stdout.write("Похоже на невалидный токен (ожидается формат <id>:<secret>). Попробуйте еще раз.\n");
185
+ continue;
186
+ }
187
+ return token;
188
+ }
189
+ }
190
+ async function askAllowedUserId() {
191
+ for (;;) {
192
+ const allowedUserId = await askVisible("Введите ваш Telegram User ID (можно узнать у @userinfobot).\n> ");
193
+ if (!isPositiveInteger(allowedUserId)) {
194
+ process.stdout.write("Введите положительное целое число (> 0).\n");
195
+ continue;
196
+ }
197
+ return allowedUserId;
198
+ }
199
+ }
200
+ async function askApiUrl() {
201
+ const prompt = `Введите URL OpenCode API (опционально).\nНажмите Enter для значения по умолчанию: ${DEFAULT_API_URL}\n> `;
202
+ for (;;) {
203
+ const apiUrl = await askVisible(prompt);
204
+ if (!apiUrl) {
205
+ return undefined;
206
+ }
207
+ if (!isValidHttpUrl(apiUrl)) {
208
+ process.stdout.write("Введите корректный URL (http/https) или нажмите Enter для значения по умолчанию.\n");
209
+ continue;
210
+ }
211
+ return apiUrl;
212
+ }
213
+ }
214
+ async function collectWizardValues() {
215
+ process.stdout.write("Запуск first-run wizard для настройки OpenCode Telegram Bot.\n");
216
+ process.stdout.write("\n");
217
+ const token = await askToken();
218
+ const allowedUserId = await askAllowedUserId();
219
+ const apiUrl = await askApiUrl();
220
+ process.stdout.write("\n");
221
+ return {
222
+ token,
223
+ allowedUserId,
224
+ apiUrl,
225
+ };
226
+ }
227
+ function ensureInteractiveTty() {
228
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
229
+ throw new Error("Interactive wizard requires a TTY terminal. Run `opencode-telegram config` in an interactive shell.");
230
+ }
231
+ }
232
+ async function validateExistingEnv(envFilePath) {
233
+ const content = await readEnvFileIfExists(envFilePath);
234
+ if (content === null) {
235
+ return { isValid: false, reason: "Missing .env" };
236
+ }
237
+ const parsed = dotenv.parse(content);
238
+ return validateRuntimeEnvValues(parsed);
239
+ }
240
+ async function runWizardAndPersist(runtimePaths) {
241
+ ensureInteractiveTty();
242
+ const [existingContent, modelDefaults, wizardValues] = await Promise.all([
243
+ readEnvFileIfExists(runtimePaths.envFilePath),
244
+ loadModelDefaultsFromEnvExample(),
245
+ collectWizardValues(),
246
+ ]);
247
+ const existingParsed = existingContent ? dotenv.parse(existingContent) : {};
248
+ const provider = existingParsed.OPENCODE_MODEL_PROVIDER || modelDefaults.provider;
249
+ const modelId = existingParsed.OPENCODE_MODEL_ID || modelDefaults.modelId;
250
+ const envValues = {
251
+ TELEGRAM_BOT_TOKEN: wizardValues.token,
252
+ TELEGRAM_ALLOWED_USER_ID: wizardValues.allowedUserId,
253
+ OPENCODE_API_URL: wizardValues.apiUrl,
254
+ OPENCODE_MODEL_PROVIDER: provider,
255
+ OPENCODE_MODEL_ID: modelId,
256
+ };
257
+ const envContent = buildEnvFileContent(existingContent ?? "", envValues);
258
+ await writeFileAtomically(runtimePaths.envFilePath, envContent);
259
+ await ensureSettingsFile(runtimePaths.settingsFilePath);
260
+ process.stdout.write(`Конфигурация сохранена:\n- ${runtimePaths.envFilePath}\n- ${runtimePaths.settingsFilePath}\n`);
261
+ }
262
+ export async function ensureRuntimeConfigForStart() {
263
+ const runtimePaths = getRuntimePaths();
264
+ if (runtimePaths.mode !== "installed") {
265
+ return;
266
+ }
267
+ const validationResult = await validateExistingEnv(runtimePaths.envFilePath);
268
+ if (validationResult.isValid) {
269
+ await ensureSettingsFile(runtimePaths.settingsFilePath);
270
+ return;
271
+ }
272
+ process.stdout.write("Приложение еще не сконфигурировано. Запускаю wizard...\n");
273
+ await runWizardAndPersist(runtimePaths);
274
+ }
275
+ export async function runConfigWizardCommand() {
276
+ const runtimePaths = getRuntimePaths();
277
+ await runWizardAndPersist(runtimePaths);
278
+ }
@@ -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-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,10 @@
1
+ import { getCurrentSession as getSettingsSession, setCurrentSession as setSettingsSession, clearSession as clearSettingsSession, } from "../settings/manager.js";
2
+ export function setCurrentSession(sessionInfo) {
3
+ setSettingsSession(sessionInfo);
4
+ }
5
+ export function getCurrentSession() {
6
+ return getSettingsSession() ?? null;
7
+ }
8
+ export function clearSession() {
9
+ clearSettingsSession();
10
+ }
@@ -0,0 +1,24 @@
1
+ const state = new Map();
2
+ export function getSessionState(sessionId) {
3
+ if (!state.has(sessionId)) {
4
+ state.set(sessionId, {
5
+ todos: [],
6
+ });
7
+ }
8
+ return state.get(sessionId);
9
+ }
10
+ export function updateLastUserMessage(sessionId, message) {
11
+ const sessionState = getSessionState(sessionId);
12
+ sessionState.lastUserMessage = message;
13
+ }
14
+ export function updateTodos(sessionId, todos) {
15
+ const sessionState = getSessionState(sessionId);
16
+ sessionState.todos = todos;
17
+ }
18
+ export function updateTokens(sessionId, input, limit) {
19
+ const sessionState = getSessionState(sessionId);
20
+ sessionState.lastTokens = { input, limit };
21
+ }
22
+ export function clearSessionState(sessionId) {
23
+ state.delete(sessionId);
24
+ }
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+ import { getRuntimePaths } from "../runtime/paths.js";
3
+ import { logger } from "../utils/logger.js";
4
+ function getSettingsFilePath() {
5
+ return getRuntimePaths().settingsFilePath;
6
+ }
7
+ async function readSettingsFile() {
8
+ try {
9
+ const fs = await import("fs/promises");
10
+ const content = await fs.readFile(getSettingsFilePath(), "utf-8");
11
+ return JSON.parse(content);
12
+ }
13
+ catch (error) {
14
+ if (error.code !== "ENOENT") {
15
+ logger.error("[SettingsManager] Error reading settings file:", error);
16
+ }
17
+ return {};
18
+ }
19
+ }
20
+ function writeSettingsFile(settings) {
21
+ import("fs/promises").then((fs) => {
22
+ const settingsFilePath = getSettingsFilePath();
23
+ fs.mkdir(path.dirname(settingsFilePath), { recursive: true })
24
+ .then(() => fs.writeFile(settingsFilePath, JSON.stringify(settings, null, 2)))
25
+ .catch((err) => {
26
+ logger.error("[SettingsManager] Error writing settings file:", err);
27
+ });
28
+ });
29
+ }
30
+ let currentSettings = {};
31
+ export function getCurrentProject() {
32
+ return currentSettings.currentProject;
33
+ }
34
+ export function setCurrentProject(projectInfo) {
35
+ currentSettings.currentProject = projectInfo;
36
+ writeSettingsFile(currentSettings);
37
+ }
38
+ export function clearProject() {
39
+ currentSettings.currentProject = undefined;
40
+ writeSettingsFile(currentSettings);
41
+ }
42
+ export function getCurrentSession() {
43
+ return currentSettings.currentSession;
44
+ }
45
+ export function setCurrentSession(sessionInfo) {
46
+ currentSettings.currentSession = sessionInfo;
47
+ writeSettingsFile(currentSettings);
48
+ }
49
+ export function clearSession() {
50
+ currentSettings.currentSession = undefined;
51
+ writeSettingsFile(currentSettings);
52
+ }
53
+ export function getCurrentAgent() {
54
+ return currentSettings.currentAgent;
55
+ }
56
+ export function setCurrentAgent(agentName) {
57
+ currentSettings.currentAgent = agentName;
58
+ writeSettingsFile(currentSettings);
59
+ }
60
+ export function clearCurrentAgent() {
61
+ currentSettings.currentAgent = undefined;
62
+ writeSettingsFile(currentSettings);
63
+ }
64
+ export function getCurrentModel() {
65
+ return currentSettings.currentModel;
66
+ }
67
+ export function setCurrentModel(modelInfo) {
68
+ currentSettings.currentModel = modelInfo;
69
+ writeSettingsFile(currentSettings);
70
+ }
71
+ export function clearCurrentModel() {
72
+ currentSettings.currentModel = undefined;
73
+ writeSettingsFile(currentSettings);
74
+ }
75
+ export function getPinnedMessageId() {
76
+ return currentSettings.pinnedMessageId;
77
+ }
78
+ export function setPinnedMessageId(messageId) {
79
+ currentSettings.pinnedMessageId = messageId;
80
+ writeSettingsFile(currentSettings);
81
+ }
82
+ export function clearPinnedMessageId() {
83
+ currentSettings.pinnedMessageId = undefined;
84
+ writeSettingsFile(currentSettings);
85
+ }
86
+ export function getServerProcess() {
87
+ return currentSettings.serverProcess;
88
+ }
89
+ export function setServerProcess(processInfo) {
90
+ currentSettings.serverProcess = processInfo;
91
+ writeSettingsFile(currentSettings);
92
+ }
93
+ export function clearServerProcess() {
94
+ currentSettings.serverProcess = undefined;
95
+ writeSettingsFile(currentSettings);
96
+ }
97
+ export async function loadSettings() {
98
+ currentSettings = await readSettingsFile();
99
+ }
@@ -0,0 +1,44 @@
1
+ export function formatStatus(data) {
2
+ const completedTasks = data.todos.filter((t) => t.status === "completed").length;
3
+ const totalTasks = data.todos.length;
4
+ const tasksStr = totalTasks > 0 ? `${completedTasks}/${totalTasks}` : "0/0";
5
+ const contextPercentage = data.contextMax > 0 ? (data.contextUsed / data.contextMax) * 100 : 0;
6
+ const contextBar = formatContextBar(contextPercentage);
7
+ const lastUpdated = data.lastUpdated
8
+ ? new Date(data.lastUpdated).toLocaleTimeString("ru-RU")
9
+ : new Date().toLocaleTimeString("ru-RU");
10
+ return `📊 Статус сессии
11
+
12
+ 🏗 Проект: ${formatProjectName(data.projectName)}
13
+ 🤖 Модель: ${formatModel(data.model)}
14
+ 📝 Сессия: ${data.sessionTitle}
15
+ ✅ Задачи: ${tasksStr}
16
+ 🔧 Режим: ${data.agent}
17
+ 📦 Контекст: ${contextPercentage.toFixed(0)}% ${contextBar}
18
+
19
+ 🕐 Обновлено: ${lastUpdated}`;
20
+ }
21
+ export function formatProjectName(name) {
22
+ if (name.length <= 30) {
23
+ return name;
24
+ }
25
+ const parts = name.split(/[\/\\]/);
26
+ const fileName = parts[parts.length - 1] || name;
27
+ if (fileName.length <= 30) {
28
+ return fileName;
29
+ }
30
+ return `${fileName.substring(0, 20)}...`;
31
+ }
32
+ export function formatModel(model) {
33
+ return model;
34
+ }
35
+ export function formatContextUsage(used, max) {
36
+ const percentage = max > 0 ? (used / max) * 100 : 0;
37
+ return `${percentage.toFixed(0)}% ${formatContextBar(percentage)}`;
38
+ }
39
+ function formatContextBar(percentage) {
40
+ const totalBars = 10;
41
+ const filledBars = Math.round((percentage / 100) * totalBars);
42
+ const emptyBars = totalBars - filledBars;
43
+ return `[${"█".repeat(filledBars)}${"░".repeat(emptyBars)}]`;
44
+ }