@blunking/codexlink 0.1.0
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/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/blun-codex.js +26 -0
- package/blun-codex.cmd +3 -0
- package/blun-codex.ps1 +110 -0
- package/package.json +37 -0
- package/profiles/default.json +20 -0
- package/start-codex-agent.ps1 +727 -0
- package/start-codex.cmd +2 -0
- package/telegram-doctor.ps1 +125 -0
- package/telegram-plugin/.codex-plugin/plugin.json +6 -0
- package/telegram-plugin/.env.example +9 -0
- package/telegram-plugin/.mcp.json +8 -0
- package/telegram-plugin/README.md +68 -0
- package/telegram-plugin/app-server-cli.js +98 -0
- package/telegram-plugin/dispatcher.js +37 -0
- package/telegram-plugin/lib/app-server-client.js +290 -0
- package/telegram-plugin/lib/bridge.js +944 -0
- package/telegram-plugin/lib/codex.js +185 -0
- package/telegram-plugin/lib/env.js +46 -0
- package/telegram-plugin/lib/paths.js +45 -0
- package/telegram-plugin/lib/sidecars.js +142 -0
- package/telegram-plugin/lib/storage.js +49 -0
- package/telegram-plugin/lib/telegram.js +37 -0
- package/telegram-plugin/package.json +10 -0
- package/telegram-plugin/poller.js +37 -0
- package/telegram-plugin/responder.js +37 -0
- package/telegram-plugin/server.js +140 -0
- package/telegram-plugin/sidecar-manager.js +8 -0
- package/telegram-status.ps1 +160 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { startTextTurnOverWs } from "./app-server-client.js";
|
|
5
|
+
|
|
6
|
+
function buildPrompt(config, message) {
|
|
7
|
+
return [
|
|
8
|
+
"[BLUN Telegram Inbound]",
|
|
9
|
+
`Agent: ${config.agentName}`,
|
|
10
|
+
...(config.lane ? [`Lane: ${config.lane}`] : []),
|
|
11
|
+
`Chat ID: ${message.chatId}`,
|
|
12
|
+
`Message ID: ${message.messageId}`,
|
|
13
|
+
`User: ${message.user || "unknown"}`,
|
|
14
|
+
`Chat Type: ${message.chatType || "unknown"}`,
|
|
15
|
+
`Conversation Key: ${message.conversationKey || `${message.chatId}:dm`}`,
|
|
16
|
+
...(message.groupTitle ? [`Group: ${message.groupTitle}`] : []),
|
|
17
|
+
...(message.telegramThreadId ? [`Telegram Thread ID: ${message.telegramThreadId}`] : []),
|
|
18
|
+
`Timestamp: ${message.ts}`,
|
|
19
|
+
"",
|
|
20
|
+
"Treat the following as a real inbound user message for this exact existing thread.",
|
|
21
|
+
"Reply naturally in-thread. Do not mention bridge transport unless relevant.",
|
|
22
|
+
"If this came from a group or topic, keep the reply scoped to that exact conversation.",
|
|
23
|
+
...(config.lane ? [`Stay strictly inside your assigned lane (${config.lane}). Do not claim ownership or make decisions for other lanes.`] : []),
|
|
24
|
+
"",
|
|
25
|
+
"Message:",
|
|
26
|
+
message.text
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function injectIntoThread(config, message, threadId) {
|
|
31
|
+
if (config.appServerWsUrl) {
|
|
32
|
+
const result = await startTextTurnOverWs({
|
|
33
|
+
wsUrl: config.appServerWsUrl,
|
|
34
|
+
threadId,
|
|
35
|
+
text: buildPrompt(config, message),
|
|
36
|
+
model: config.model || null,
|
|
37
|
+
effort: config.reasoningEffort || null,
|
|
38
|
+
personality: config.personality || null,
|
|
39
|
+
timeoutMs: config.resumeTimeoutMs
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
ok: result.ok,
|
|
44
|
+
busy: result.busy,
|
|
45
|
+
turnId: result.turnId || "",
|
|
46
|
+
code: result.ok ? 0 : null,
|
|
47
|
+
signal: null,
|
|
48
|
+
responseText: result.ok ? `turn_started thread=${threadId}` : "",
|
|
49
|
+
stdout: "",
|
|
50
|
+
stderr: result.error ? String(result.error.message || result.error) : ""
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const safeKey = `${message.chatId}_${message.messageId}`.replace(/[^0-9A-Za-z_-]/g, "_");
|
|
55
|
+
const promptFile = `${config.paths.promptsDir}\\${safeKey}.md`;
|
|
56
|
+
const responseFile = `${config.paths.responsesDir}\\${safeKey}.txt`;
|
|
57
|
+
writeFileSync(promptFile, buildPrompt(config, message), "utf8");
|
|
58
|
+
|
|
59
|
+
return await new Promise((resolve) => {
|
|
60
|
+
let timedOut = false;
|
|
61
|
+
let stdout = "";
|
|
62
|
+
let stderr = "";
|
|
63
|
+
let settled = false;
|
|
64
|
+
const command = resolveCodexCommand(config.codexBin);
|
|
65
|
+
const child = spawn(
|
|
66
|
+
command.file,
|
|
67
|
+
command.args.concat([
|
|
68
|
+
"exec",
|
|
69
|
+
"resume",
|
|
70
|
+
"--skip-git-repo-check",
|
|
71
|
+
"-o",
|
|
72
|
+
responseFile,
|
|
73
|
+
threadId,
|
|
74
|
+
"-"
|
|
75
|
+
]),
|
|
76
|
+
command.options
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const finish = (value) => {
|
|
80
|
+
if (settled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
settled = true;
|
|
84
|
+
resolve(value);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
child.on("error", (error) => {
|
|
88
|
+
finish({
|
|
89
|
+
ok: false,
|
|
90
|
+
busy: false,
|
|
91
|
+
code: null,
|
|
92
|
+
signal: null,
|
|
93
|
+
responseText: "",
|
|
94
|
+
stdout: stdout.trim(),
|
|
95
|
+
stderr: `${stderr}\n${error}`.trim()
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
timedOut = true;
|
|
101
|
+
child.kill("SIGTERM");
|
|
102
|
+
const hardKill = setTimeout(() => {
|
|
103
|
+
child.kill("SIGKILL");
|
|
104
|
+
}, 3000);
|
|
105
|
+
hardKill.unref?.();
|
|
106
|
+
}, config.resumeTimeoutMs);
|
|
107
|
+
|
|
108
|
+
child.stdout.on("data", (chunk) => {
|
|
109
|
+
stdout += chunk.toString();
|
|
110
|
+
});
|
|
111
|
+
child.stderr.on("data", (chunk) => {
|
|
112
|
+
stderr += chunk.toString();
|
|
113
|
+
});
|
|
114
|
+
child.stdin.write(readFileSync(promptFile, "utf8"));
|
|
115
|
+
child.stdin.end();
|
|
116
|
+
|
|
117
|
+
child.on("close", (code, signal) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
const responseText = existsSync(responseFile) ? readFileSync(responseFile, "utf8").trim() : "";
|
|
120
|
+
if (timedOut) {
|
|
121
|
+
finish({
|
|
122
|
+
ok: false,
|
|
123
|
+
busy: true,
|
|
124
|
+
code,
|
|
125
|
+
signal,
|
|
126
|
+
responseText,
|
|
127
|
+
stdout: stdout.trim(),
|
|
128
|
+
stderr: stderr.trim()
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
finish({
|
|
133
|
+
ok: code === 0,
|
|
134
|
+
busy: false,
|
|
135
|
+
code,
|
|
136
|
+
signal,
|
|
137
|
+
responseText,
|
|
138
|
+
stdout: stdout.trim(),
|
|
139
|
+
stderr: stderr.trim()
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveCodexCommand(rawCodexBin) {
|
|
146
|
+
const requested = (rawCodexBin || "codex").trim();
|
|
147
|
+
if (process.platform !== "win32") {
|
|
148
|
+
return {
|
|
149
|
+
file: requested,
|
|
150
|
+
args: [],
|
|
151
|
+
options: {}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const appData = process.env.APPDATA || "";
|
|
156
|
+
const preferredCandidates = [
|
|
157
|
+
requested,
|
|
158
|
+
requested.endsWith(".cmd") ? requested : `${requested}.cmd`,
|
|
159
|
+
appData ? join(appData, "npm", "codex.cmd") : "",
|
|
160
|
+
appData ? join(appData, "npm", "codex") : ""
|
|
161
|
+
].filter(Boolean);
|
|
162
|
+
|
|
163
|
+
for (const candidate of preferredCandidates) {
|
|
164
|
+
if (candidate === requested || existsSync(candidate)) {
|
|
165
|
+
if (candidate.toLowerCase().endsWith(".cmd")) {
|
|
166
|
+
return {
|
|
167
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
168
|
+
args: ["/d", "/s", "/c", candidate],
|
|
169
|
+
options: { windowsHide: true }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
file: candidate,
|
|
174
|
+
args: [],
|
|
175
|
+
options: { windowsHide: true }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
182
|
+
args: ["/d", "/s", "/c", requested],
|
|
183
|
+
options: { windowsHide: true }
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { ensureStateLayout, getPaths } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
function readDotEnvFile(path) {
|
|
5
|
+
const values = {};
|
|
6
|
+
try {
|
|
7
|
+
const text = readFileSync(path, "utf8");
|
|
8
|
+
for (const line of text.split(/\r?\n/)) {
|
|
9
|
+
if (!line || line.trim().startsWith("#") || !line.includes("=")) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const [rawKey, ...rest] = line.split("=");
|
|
13
|
+
values[rawKey.trim()] = rest.join("=").trim();
|
|
14
|
+
}
|
|
15
|
+
} catch {
|
|
16
|
+
return values;
|
|
17
|
+
}
|
|
18
|
+
return values;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadConfig() {
|
|
22
|
+
ensureStateLayout();
|
|
23
|
+
const paths = getPaths();
|
|
24
|
+
const fileEnv = readDotEnvFile(paths.envFile);
|
|
25
|
+
const legacyEnv = readDotEnvFile(`${paths.legacyRoot}\\.env`);
|
|
26
|
+
const fallbackEnv = { ...legacyEnv };
|
|
27
|
+
delete fallbackEnv.BLUN_TELEGRAM_AGENT_NAME;
|
|
28
|
+
delete fallbackEnv.BLUN_TELEGRAM_STATE_DIR;
|
|
29
|
+
delete fallbackEnv.BLUN_TELEGRAM_THREAD_ID;
|
|
30
|
+
const env = { ...fallbackEnv, ...fileEnv, ...process.env };
|
|
31
|
+
return {
|
|
32
|
+
paths,
|
|
33
|
+
agentName: env.BLUN_TELEGRAM_AGENT_NAME?.trim() || "default",
|
|
34
|
+
lane: env.BLUN_CODEX_LANE?.trim() || "",
|
|
35
|
+
botToken: env.BLUN_TELEGRAM_BOT_TOKEN?.trim() || "",
|
|
36
|
+
allowedChatId: env.BLUN_TELEGRAM_ALLOWED_CHAT_ID?.trim() || "",
|
|
37
|
+
codexBin: env.BLUN_TELEGRAM_CODEX_BIN?.trim() || "codex",
|
|
38
|
+
appServerWsUrl: env.BLUN_TELEGRAM_APP_SERVER_WS_URL?.trim() || "",
|
|
39
|
+
currentThreadId: env.BLUN_TELEGRAM_THREAD_ID?.trim() || process.env.CODEX_THREAD_ID?.trim() || "",
|
|
40
|
+
resumeTimeoutMs: Number.parseInt(env.BLUN_TELEGRAM_RESUME_TIMEOUT_MS || "15000", 10) || 15000,
|
|
41
|
+
pluginMode: env.BLUN_TELEGRAM_PLUGIN_MODE?.trim() || "inherit",
|
|
42
|
+
model: env.BLUN_CODEX_MODEL?.trim() || "",
|
|
43
|
+
reasoningEffort: env.BLUN_CODEX_REASONING_EFFORT?.trim() || "",
|
|
44
|
+
personality: env.BLUN_CODEX_PERSONALITY?.trim() || ""
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export function getStateDir() {
|
|
6
|
+
return process.env.BLUN_TELEGRAM_STATE_DIR?.trim() || join(homedir(), ".codex", "channels", "codexlink-telegram");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getPaths() {
|
|
10
|
+
const root = getStateDir();
|
|
11
|
+
const codexHome = join(homedir(), ".codex");
|
|
12
|
+
return {
|
|
13
|
+
root,
|
|
14
|
+
legacyRoot: join(codexHome, "channels", "codexlink-telegram"),
|
|
15
|
+
codexHome,
|
|
16
|
+
sessionsDir: join(codexHome, "sessions"),
|
|
17
|
+
envFile: join(root, ".env"),
|
|
18
|
+
stateFile: join(root, "state.json"),
|
|
19
|
+
inboxFile: join(root, "inbox.jsonl"),
|
|
20
|
+
outboxFile: join(root, "outbox.jsonl"),
|
|
21
|
+
activityFile: join(root, "activity.log"),
|
|
22
|
+
pollerPidFile: join(root, "poller.pid"),
|
|
23
|
+
dispatcherPidFile: join(root, "dispatcher.pid"),
|
|
24
|
+
responderPidFile: join(root, "responder.pid"),
|
|
25
|
+
pollerStdoutFile: join(root, "poller.stdout.log"),
|
|
26
|
+
pollerStderrFile: join(root, "poller.stderr.log"),
|
|
27
|
+
dispatcherStdoutFile: join(root, "dispatcher.stdout.log"),
|
|
28
|
+
dispatcherStderrFile: join(root, "dispatcher.stderr.log"),
|
|
29
|
+
responderStdoutFile: join(root, "responder.stdout.log"),
|
|
30
|
+
responderStderrFile: join(root, "responder.stderr.log"),
|
|
31
|
+
historyFile: join(codexHome, "history.jsonl"),
|
|
32
|
+
promptsDir: join(root, "prompts"),
|
|
33
|
+
responsesDir: join(root, "responses")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ensureStateLayout() {
|
|
38
|
+
const paths = getPaths();
|
|
39
|
+
for (const dir of [paths.root, paths.promptsDir, paths.responsesDir]) {
|
|
40
|
+
if (!existsSync(dir)) {
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return paths;
|
|
45
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { appendLog } from "./storage.js";
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pluginRoot = join(here, "..");
|
|
9
|
+
|
|
10
|
+
function readPid(path) {
|
|
11
|
+
try {
|
|
12
|
+
return Number.parseInt(readFileSync(path, "utf8").trim(), 10) || 0;
|
|
13
|
+
} catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isPidAlive(pid) {
|
|
19
|
+
if (!pid || pid <= 0) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stopPid(pid) {
|
|
31
|
+
if (!isPidAlive(pid)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, "SIGTERM");
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
process.kill(pid, "SIGKILL");
|
|
41
|
+
} catch {
|
|
42
|
+
// Process may already be gone.
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureSidecar(scriptName, pidFile, stdoutFile, stderrFile, config, options = {}) {
|
|
48
|
+
const forceRestart = Boolean(options.forceRestart);
|
|
49
|
+
const existingPid = readPid(pidFile);
|
|
50
|
+
if (isPidAlive(existingPid)) {
|
|
51
|
+
if (!forceRestart) {
|
|
52
|
+
return { started: false, pid: existingPid, reason: "already_running" };
|
|
53
|
+
}
|
|
54
|
+
stopPid(existingPid);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const env = {
|
|
58
|
+
...process.env,
|
|
59
|
+
BLUN_TELEGRAM_AGENT_NAME: config.agentName || "default",
|
|
60
|
+
BLUN_TELEGRAM_STATE_DIR: config.paths.root,
|
|
61
|
+
BLUN_TELEGRAM_BOT_TOKEN: config.botToken || "",
|
|
62
|
+
BLUN_TELEGRAM_ALLOWED_CHAT_ID: config.allowedChatId || "",
|
|
63
|
+
BLUN_TELEGRAM_APP_SERVER_WS_URL: config.appServerWsUrl || "",
|
|
64
|
+
BLUN_TELEGRAM_CODEX_BIN: config.codexBin || "codex",
|
|
65
|
+
BLUN_TELEGRAM_RESUME_TIMEOUT_MS: String(config.resumeTimeoutMs || 15000),
|
|
66
|
+
BLUN_TELEGRAM_PLUGIN_MODE: config.pluginMode || "plugin",
|
|
67
|
+
BLUN_CODEX_MODEL: config.model || "",
|
|
68
|
+
BLUN_CODEX_REASONING_EFFORT: config.reasoningEffort || "",
|
|
69
|
+
BLUN_CODEX_PERSONALITY: config.personality || ""
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (config.currentThreadId) {
|
|
73
|
+
env.BLUN_TELEGRAM_THREAD_ID = config.currentThreadId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const child = spawn(
|
|
77
|
+
process.execPath,
|
|
78
|
+
[join(pluginRoot, scriptName)],
|
|
79
|
+
{
|
|
80
|
+
cwd: pluginRoot,
|
|
81
|
+
env,
|
|
82
|
+
detached: true,
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
stdio: [
|
|
85
|
+
"ignore",
|
|
86
|
+
openSync(stdoutFile, "a"),
|
|
87
|
+
openSync(stderrFile, "a")
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
child.unref();
|
|
92
|
+
writeFileSync(pidFile, `${child.pid}\n`, "utf8");
|
|
93
|
+
return { started: true, pid: child.pid, reason: "spawned" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function ensureBackgroundSidecars(config) {
|
|
97
|
+
if (config.pluginMode !== "plugin") {
|
|
98
|
+
return { ok: true, enabled: false, reason: "plugin_mode_not_enabled" };
|
|
99
|
+
}
|
|
100
|
+
if (!config.botToken) {
|
|
101
|
+
appendLog(config.paths.activityFile, "PLUGIN_AUTOSTART_SKIPPED missing_bot_token");
|
|
102
|
+
return { ok: false, enabled: false, reason: "missing_bot_token" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const poller = ensureSidecar(
|
|
106
|
+
"poller.js",
|
|
107
|
+
config.paths.pollerPidFile,
|
|
108
|
+
config.paths.pollerStdoutFile,
|
|
109
|
+
config.paths.pollerStderrFile,
|
|
110
|
+
config,
|
|
111
|
+
{ forceRestart: true }
|
|
112
|
+
);
|
|
113
|
+
const dispatcher = ensureSidecar(
|
|
114
|
+
"dispatcher.js",
|
|
115
|
+
config.paths.dispatcherPidFile,
|
|
116
|
+
config.paths.dispatcherStdoutFile,
|
|
117
|
+
config.paths.dispatcherStderrFile,
|
|
118
|
+
config,
|
|
119
|
+
{ forceRestart: true }
|
|
120
|
+
);
|
|
121
|
+
const responder = ensureSidecar(
|
|
122
|
+
"responder.js",
|
|
123
|
+
config.paths.responderPidFile,
|
|
124
|
+
config.paths.responderStdoutFile,
|
|
125
|
+
config.paths.responderStderrFile,
|
|
126
|
+
config,
|
|
127
|
+
{ forceRestart: true }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
appendLog(
|
|
131
|
+
config.paths.activityFile,
|
|
132
|
+
`PLUGIN_AUTOSTART poller=${poller.pid || 0}:${poller.reason} dispatcher=${dispatcher.pid || 0}:${dispatcher.reason} responder=${responder.pid || 0}:${responder.reason}`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
enabled: true,
|
|
138
|
+
poller,
|
|
139
|
+
dispatcher,
|
|
140
|
+
responder
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export function nowIso() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function loadJson(path, fallback) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveJson(path, value) {
|
|
17
|
+
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function appendJsonl(path, value) {
|
|
21
|
+
appendFileSync(path, `${JSON.stringify(value)}\n`, "utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function appendLog(path, message) {
|
|
25
|
+
appendFileSync(path, `${nowIso()} ${message}\n`, "utf8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readTail(path, lines = 20) {
|
|
29
|
+
if (!existsSync(path)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const text = readFileSync(path, "utf8");
|
|
33
|
+
return text.split(/\r?\n/).filter(Boolean).slice(-lines);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function defaultState() {
|
|
37
|
+
return {
|
|
38
|
+
offset: 0,
|
|
39
|
+
currentThreadId: "",
|
|
40
|
+
queue: [],
|
|
41
|
+
pendingReplies: [],
|
|
42
|
+
replyOffsets: {},
|
|
43
|
+
replyBuffers: {},
|
|
44
|
+
lastInbound: null,
|
|
45
|
+
lastOutbound: null,
|
|
46
|
+
lastPollAt: null,
|
|
47
|
+
lastInjectAt: null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
function requireToken(config) {
|
|
2
|
+
if (!config.botToken) {
|
|
3
|
+
throw new Error(`BLUN_TELEGRAM_BOT_TOKEN is missing in ${config.paths.envFile}`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function telegramRequest(config, method, body) {
|
|
8
|
+
requireToken(config);
|
|
9
|
+
const response = await fetch(`https://api.telegram.org/bot${config.botToken}/${method}`, {
|
|
10
|
+
method: body ? "POST" : "GET",
|
|
11
|
+
headers: body ? { "Content-Type": "application/json; charset=utf-8" } : undefined,
|
|
12
|
+
body: body ? JSON.stringify(body) : undefined
|
|
13
|
+
});
|
|
14
|
+
const json = await response.json();
|
|
15
|
+
if (!json.ok) {
|
|
16
|
+
throw new Error(`Telegram ${method} failed: ${json.description || response.status}`);
|
|
17
|
+
}
|
|
18
|
+
return json.result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getUpdates(config, offset) {
|
|
22
|
+
return telegramRequest(config, "getUpdates", {
|
|
23
|
+
offset,
|
|
24
|
+
timeout: 1,
|
|
25
|
+
allowed_updates: ["message"]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function sendMessage(config, { chatId, text, replyToMessageId, telegramThreadId }) {
|
|
30
|
+
return telegramRequest(config, "sendMessage", {
|
|
31
|
+
chat_id: chatId,
|
|
32
|
+
text,
|
|
33
|
+
disable_web_page_preview: true,
|
|
34
|
+
...(replyToMessageId ? { reply_to_message_id: Number(replyToMessageId), allow_sending_without_reply: true } : {}),
|
|
35
|
+
...(telegramThreadId ? { message_thread_id: Number(telegramThreadId) } : {})
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { pollOnce } from "./lib/bridge.js";
|
|
3
|
+
|
|
4
|
+
const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_POLL_INTERVAL_MS || "1500", 10) || 1500;
|
|
5
|
+
let stopping = false;
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.on("SIGINT", () => {
|
|
12
|
+
stopping = true;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.on("SIGTERM", () => {
|
|
16
|
+
stopping = true;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
while (!stopping) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await pollOnce();
|
|
23
|
+
if (result.captured > 0 || result.ignored > 0) {
|
|
24
|
+
console.log(JSON.stringify({ ts: new Date().toISOString(), kind: "poll", result }));
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(JSON.stringify({
|
|
28
|
+
ts: new Date().toISOString(),
|
|
29
|
+
kind: "error",
|
|
30
|
+
error: `${error}`
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
await sleep(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await main();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { relayRepliesOnce } from "./lib/bridge.js";
|
|
3
|
+
|
|
4
|
+
const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_REPLY_INTERVAL_MS || "1500", 10) || 1500;
|
|
5
|
+
let stopping = false;
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.on("SIGINT", () => {
|
|
12
|
+
stopping = true;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
process.on("SIGTERM", () => {
|
|
16
|
+
stopping = true;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
while (!stopping) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await relayRepliesOnce();
|
|
23
|
+
if (result.status !== "empty" && result.delivered > 0) {
|
|
24
|
+
console.log(JSON.stringify({ ts: new Date().toISOString(), kind: "reply", result }));
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(JSON.stringify({
|
|
28
|
+
ts: new Date().toISOString(),
|
|
29
|
+
kind: "error",
|
|
30
|
+
error: `${error}`
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
await sleep(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await main();
|