@grinev/opencode-telegram-bot 0.22.0 → 0.22.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.
@@ -79,12 +79,61 @@ const APPLICATION_TEXT_MIME_TYPES = new Set([
79
79
  "application/x-yaml",
80
80
  "application/sql",
81
81
  ]);
82
- export function isTextMimeType(mimeType) {
82
+ const TEXT_FILE_EXTENSIONS = new Set([
83
+ "svelte",
84
+ "vue",
85
+ "ts",
86
+ "tsx",
87
+ "jsx",
88
+ "mjs",
89
+ "cjs",
90
+ "go",
91
+ "rs",
92
+ "rb",
93
+ "py",
94
+ "java",
95
+ "c",
96
+ "cpp",
97
+ "h",
98
+ "hpp",
99
+ "cs",
100
+ "swift",
101
+ "kt",
102
+ "kts",
103
+ "sh",
104
+ "bash",
105
+ "yaml",
106
+ "yml",
107
+ "toml",
108
+ "ini",
109
+ "cfg",
110
+ "md",
111
+ "mdx",
112
+ "css",
113
+ "scss",
114
+ "less",
115
+ "html",
116
+ "htm",
117
+ "graphql",
118
+ "gql",
119
+ "proto",
120
+ "gradle",
121
+ ]);
122
+ export function isTextMimeType(mimeType, filename) {
83
123
  if (!mimeType) {
84
124
  return false;
85
125
  }
86
126
  if (mimeType.startsWith("text/")) {
87
127
  return true;
88
128
  }
89
- return APPLICATION_TEXT_MIME_TYPES.has(mimeType);
129
+ if (APPLICATION_TEXT_MIME_TYPES.has(mimeType)) {
130
+ return true;
131
+ }
132
+ if (filename) {
133
+ const ext = filename.split(".").pop()?.toLowerCase();
134
+ if (ext && TEXT_FILE_EXTENSIONS.has(ext)) {
135
+ return true;
136
+ }
137
+ }
138
+ return false;
90
139
  }
@@ -100,12 +100,31 @@ function formatDateTime(dateIso, timezone) {
100
100
  return dateIso;
101
101
  }
102
102
  }
103
+ const TASK_DETAIL_PROMPT_BYTE_BUDGET = 3400;
104
+ function truncatePromptForDetails(prompt) {
105
+ if (Buffer.byteLength(prompt, "utf-8") <= TASK_DETAIL_PROMPT_BYTE_BUDGET) {
106
+ return prompt;
107
+ }
108
+ const budget = TASK_DETAIL_PROMPT_BYTE_BUDGET - 3;
109
+ let lo = 0;
110
+ let hi = prompt.length;
111
+ while (lo < hi) {
112
+ const mid = (lo + hi + 1) >>> 1;
113
+ if (Buffer.byteLength(prompt.slice(0, mid), "utf-8") <= budget) {
114
+ lo = mid;
115
+ }
116
+ else {
117
+ hi = mid - 1;
118
+ }
119
+ }
120
+ return `${prompt.slice(0, lo)}...`;
121
+ }
103
122
  function formatTaskDetails(task) {
104
123
  const variant = task.model.variant ? ` (${task.model.variant})` : "";
105
124
  const model = `${task.model.providerID}/${task.model.modelID}${variant}`;
106
125
  const cronLine = task.kind === "cron" ? `${t("tasklist.details.cron", { cron: task.cron })}\n` : "";
107
126
  return t("tasklist.details", {
108
- prompt: task.prompt,
127
+ prompt: truncatePromptForDetails(task.prompt),
109
128
  project: `${task.projectWorktree}\n${t("status.line.model", { model })}`,
110
129
  schedule: task.scheduleSummary,
111
130
  cronLine,
@@ -18,7 +18,7 @@ export async function handleDocumentMessage(ctx, deps) {
18
18
  const mimeType = doc.mime_type || "";
19
19
  const filename = doc.file_name || "document";
20
20
  try {
21
- if (isTextMimeType(mimeType)) {
21
+ if (isTextMimeType(mimeType, filename)) {
22
22
  if (!isFileSizeAllowed(doc.file_size, config.files.maxFileSizeKb)) {
23
23
  logger.warn(`[Document] Text file too large: ${filename} (${doc.file_size} bytes > ${config.files.maxFileSizeKb}KB)`);
24
24
  await ctx.reply(t("bot.text_file_too_large", { maxSizeKb: String(config.files.maxFileSizeKb) }));
@@ -137,7 +137,7 @@ export class MediaGroupAttachmentHandler {
137
137
  const document = item.document;
138
138
  const mimeType = document.mime_type || "";
139
139
  const filename = document.file_name || "document";
140
- if (isTextMimeType(mimeType)) {
140
+ if (isTextMimeType(mimeType, filename)) {
141
141
  if (!isFileSizeAllowed(document.file_size, config.files.maxFileSizeKb)) {
142
142
  return { reason: "text_file_too_large" };
143
143
  }
@@ -26,19 +26,10 @@ export class CompactProgressStreamer {
26
26
  this.editText = editText;
27
27
  }
28
28
  updateActivity(sessionId, activity) {
29
- const normalizedActivity = activity.trim();
30
- if (!sessionId || !normalizedActivity) {
31
- return;
32
- }
33
- const state = this.getOrCreateState(sessionId);
34
- state.latestText = t("progress.compact.activity", {
35
- header: t("progress.compact.working_header"),
36
- activity: normalizedActivity,
37
- });
38
- this.ensureTimer(state);
29
+ this.updateActivityState(sessionId, activity, true);
39
30
  }
40
31
  updateThinking(sessionId) {
41
- this.updateActivity(sessionId, t("progress.compact.thinking"));
32
+ this.updateActivityState(sessionId, t("progress.compact.thinking"), false);
42
33
  }
43
34
  updateResponding(sessionId) {
44
35
  this.updateActivity(sessionId, t("progress.compact.responding"));
@@ -105,6 +96,21 @@ export class CompactProgressStreamer {
105
96
  this.states.set(sessionId, state);
106
97
  return state;
107
98
  }
99
+ updateActivityState(sessionId, activity, createIfMissing) {
100
+ const normalizedActivity = activity.trim();
101
+ if (!sessionId || !normalizedActivity) {
102
+ return;
103
+ }
104
+ const state = createIfMissing ? this.getOrCreateState(sessionId) : this.states.get(sessionId);
105
+ if (!state) {
106
+ return;
107
+ }
108
+ state.latestText = t("progress.compact.activity", {
109
+ header: t("progress.compact.working_header"),
110
+ activity: normalizedActivity,
111
+ });
112
+ this.ensureTimer(state);
113
+ }
108
114
  ensureTimer(state) {
109
115
  if (state.cancelled || state.timer) {
110
116
  return;
@@ -1,4 +1,6 @@
1
1
  import { exec, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import * as path from "node:path";
2
4
  import { promisify } from "node:util";
3
5
  const execAsync = promisify(exec);
4
6
  const DEFAULT_OPENCODE_PORT = 4096;
@@ -25,13 +27,48 @@ export function resolveLocalOpencodeTarget(apiUrl) {
25
27
  return null;
26
28
  }
27
29
  }
30
+ function resolveWindowsOpencodeExe() {
31
+ // npm on Windows usually puts opencode.cmd on PATH (not opencode.exe).
32
+ // We locate the shim and derive the real exe path from its directory.
33
+ const pathEnv = process.env.PATH ?? "";
34
+ const pathEntries = pathEnv.split(path.delimiter).filter(Boolean);
35
+ for (const entry of pathEntries) {
36
+ const opencodeCmd = path.join(entry, "opencode.cmd");
37
+ if (!existsSync(opencodeCmd)) {
38
+ continue;
39
+ }
40
+ const candidateExe = path.join(entry, "node_modules", "opencode-ai", "bin", "opencode.exe");
41
+ if (existsSync(candidateExe)) {
42
+ return candidateExe;
43
+ }
44
+ // Found the shim but not the exe where it usually lives. Stop searching.
45
+ break;
46
+ }
47
+ return "";
48
+ }
28
49
  export function createOpencodeServeSpawnCommand(target) {
29
50
  const isWindows = process.platform === "win32";
30
51
  const port = target.port.toString();
52
+ if (isWindows) {
53
+ const resolvedExe = resolveWindowsOpencodeExe();
54
+ if (resolvedExe) {
55
+ return {
56
+ command: resolvedExe,
57
+ args: ["serve", "--port", port],
58
+ windowsHide: true,
59
+ };
60
+ }
61
+ // Safe fallback: works with default npm installs where only opencode.cmd is on PATH.
62
+ return {
63
+ command: "cmd.exe",
64
+ args: ["/c", "opencode", "serve", "--port", port],
65
+ windowsHide: true,
66
+ };
67
+ }
31
68
  return {
32
- command: isWindows ? "cmd.exe" : "opencode",
33
- args: isWindows ? ["/c", "opencode", "serve", "--port", port] : ["serve", "--port", port],
34
- windowsHide: isWindows,
69
+ command: "opencode",
70
+ args: ["serve", "--port", port],
71
+ windowsHide: false,
35
72
  };
36
73
  }
37
74
  export function startLocalOpencodeServer(target) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",