@blunking/codexlink 0.1.0 → 0.1.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/README.md +233 -115
- package/blun-codex.ps1 +140 -110
- package/package.json +4 -3
- package/start-codex-agent.ps1 +746 -710
- package/telegram-doctor.ps1 +256 -113
- package/telegram-plugin/.env.example +3 -1
- package/telegram-plugin/README.md +7 -3
- package/telegram-plugin/dispatcher.js +2 -2
- package/telegram-plugin/lib/bridge.js +138 -15
- package/telegram-plugin/lib/env.js +15 -4
- package/telegram-plugin/lib/sidecars.js +3 -1
- package/telegram-plugin/lib/storage.js +50 -49
- package/telegram-setup.ps1 +245 -0
- package/telegram-status.ps1 +30 -1
|
@@ -22,6 +22,52 @@ function pendingReplyKey(entry) {
|
|
|
22
22
|
return entry.turnId || `${entry.threadId || ""}:${entry.chatId}:${entry.messageId}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function containsToken(text, token) {
|
|
26
|
+
const value = String(token || "").trim();
|
|
27
|
+
if (!value) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
return new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, "i").test(String(text || ""));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function looksLikeEscalation(text) {
|
|
35
|
+
const value = String(text || "");
|
|
36
|
+
return [
|
|
37
|
+
"eskalation",
|
|
38
|
+
"escalation",
|
|
39
|
+
"urgent",
|
|
40
|
+
"emergency",
|
|
41
|
+
"sofort",
|
|
42
|
+
"prio 0",
|
|
43
|
+
"p0",
|
|
44
|
+
"blocker"
|
|
45
|
+
].some((token) => containsToken(value, token));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function classifyInboundRelevance(config, inbound) {
|
|
49
|
+
if (looksLikeEscalation(inbound.text)) {
|
|
50
|
+
return "escalation";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (String(inbound.chatType || "") === "private") {
|
|
54
|
+
return "direct";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const text = String(inbound.text || "");
|
|
58
|
+
const agentName = String(config.agentName || "").trim();
|
|
59
|
+
if (agentName && agentName.toLowerCase() !== "default" && containsToken(text, agentName)) {
|
|
60
|
+
return "direct";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lane = String(config.lane || "").trim();
|
|
64
|
+
if (lane && lane.toLowerCase() !== "general" && containsToken(text, lane)) {
|
|
65
|
+
return "lane";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return "ambient";
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
function statusWeight(status) {
|
|
26
72
|
switch (status) {
|
|
27
73
|
case "delivered":
|
|
@@ -96,7 +142,7 @@ function mergeQueueEntry(current, incoming) {
|
|
|
96
142
|
merged.deliveredAt = pickIsoLater(current.deliveredAt, incoming.deliveredAt);
|
|
97
143
|
merged.ts = pickIsoLater(current.ts, incoming.ts);
|
|
98
144
|
|
|
99
|
-
for (const field of ["threadId", "responsePreview", "stderr", "stdout", "chatType", "conversationKey", "groupTitle", "telegramThreadId", "senderIsBot"]) {
|
|
145
|
+
for (const field of ["threadId", "responsePreview", "stderr", "stdout", "chatType", "conversationKey", "groupTitle", "telegramThreadId", "senderIsBot", "relevance"]) {
|
|
100
146
|
if (!merged[field]) {
|
|
101
147
|
merged[field] = current[field] || incoming[field] || null;
|
|
102
148
|
}
|
|
@@ -300,6 +346,7 @@ function normalizeInbound(message) {
|
|
|
300
346
|
userId: message.from?.id ? String(message.from.id) : "",
|
|
301
347
|
text,
|
|
302
348
|
ts: nowIso(),
|
|
349
|
+
relevance: "ambient",
|
|
303
350
|
status: "queued",
|
|
304
351
|
attempts: 0,
|
|
305
352
|
lastAttemptAt: null
|
|
@@ -310,6 +357,14 @@ function normalizeTelegramThreadId(value) {
|
|
|
310
357
|
return String(value || "").trim();
|
|
311
358
|
}
|
|
312
359
|
|
|
360
|
+
function isAllowedChat(config, chatId) {
|
|
361
|
+
const allowed = Array.isArray(config.allowedChatIds) ? config.allowedChatIds : [];
|
|
362
|
+
if (allowed.length === 0) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
return allowed.includes(String(chatId || "").trim());
|
|
366
|
+
}
|
|
367
|
+
|
|
313
368
|
function splitTelegramText(text, maxLength = 3500) {
|
|
314
369
|
const value = String(text || "").trim();
|
|
315
370
|
if (!value) {
|
|
@@ -401,6 +456,28 @@ async function resolveThreadSessionPath(config, threadId) {
|
|
|
401
456
|
return findRolloutFile(config.paths.sessionsDir, threadId) || "";
|
|
402
457
|
}
|
|
403
458
|
|
|
459
|
+
function countOpenPendingReplies(state) {
|
|
460
|
+
return (state.pendingReplies || []).filter((entry) => !entry.sentAt && !["error", "ignored_bot", "superseded"].includes(String(entry.status || ""))).length;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function resolveSessionActivity(config, threadId) {
|
|
464
|
+
const sessionPath = await resolveThreadSessionPath(config, threadId);
|
|
465
|
+
if (!sessionPath || !existsSync(sessionPath)) {
|
|
466
|
+
return {
|
|
467
|
+
sessionPath,
|
|
468
|
+
quietMs: Number.POSITIVE_INFINITY,
|
|
469
|
+
active: false
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const quietMs = Math.max(0, Date.now() - statSync(sessionPath).mtimeMs);
|
|
474
|
+
return {
|
|
475
|
+
sessionPath,
|
|
476
|
+
quietMs,
|
|
477
|
+
active: quietMs < Number(config.idleCooldownMs || 0)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
404
481
|
function buildPendingReplyEntry(message, threadId, turnId, sessionPath, sessionOffset) {
|
|
405
482
|
return {
|
|
406
483
|
turnId: String(turnId || "").trim(),
|
|
@@ -526,7 +603,7 @@ function promoteVisibleQueuedEntry(config, state, threadId, message) {
|
|
|
526
603
|
}
|
|
527
604
|
|
|
528
605
|
async function resolveActiveThreadId(config, state, preferredThreadId) {
|
|
529
|
-
const fallbackThreadId = String(preferredThreadId ||
|
|
606
|
+
const fallbackThreadId = String(preferredThreadId || config.currentThreadId || state.currentThreadId || "").trim();
|
|
530
607
|
if (!config.appServerWsUrl) {
|
|
531
608
|
return fallbackThreadId;
|
|
532
609
|
}
|
|
@@ -584,12 +661,16 @@ export function bridgeStatus() {
|
|
|
584
661
|
const state = loadState(config);
|
|
585
662
|
const queued = state.queue.filter((item) => item.status === "queued");
|
|
586
663
|
const submitted = state.queue.filter((item) => item.status === "submitted");
|
|
664
|
+
const ambient = queued.filter((item) => item.relevance === "ambient");
|
|
587
665
|
const pendingReplies = (state.pendingReplies || []).filter((item) => !item.sentAt && item.status !== "error");
|
|
588
666
|
return {
|
|
589
667
|
agent: config.agentName,
|
|
590
668
|
allowedChatId: config.allowedChatId || null,
|
|
591
|
-
boundThreadId:
|
|
669
|
+
boundThreadId: config.currentThreadId || state.currentThreadId || null,
|
|
670
|
+
dispatchMode: config.dispatchMode,
|
|
671
|
+
idleCooldownMs: config.idleCooldownMs,
|
|
592
672
|
queueDepth: queued.length,
|
|
673
|
+
ambientQueueDepth: ambient.length,
|
|
593
674
|
submittedDepth: submitted.length,
|
|
594
675
|
pendingReplyDepth: pendingReplies.length,
|
|
595
676
|
lastInbound: state.lastInbound,
|
|
@@ -597,7 +678,7 @@ export function bridgeStatus() {
|
|
|
597
678
|
lastPollAt: state.lastPollAt,
|
|
598
679
|
lastInjectAt: state.lastInjectAt,
|
|
599
680
|
stateDir: config.paths.root,
|
|
600
|
-
note: "
|
|
681
|
+
note: "Telegram first lands in queue. Automatic delivery waits for an idle session, skips ambient group noise, and still lets escalations through."
|
|
601
682
|
};
|
|
602
683
|
}
|
|
603
684
|
|
|
@@ -682,7 +763,7 @@ export async function pollOnce() {
|
|
|
682
763
|
continue;
|
|
683
764
|
}
|
|
684
765
|
const inbound = normalizeInbound(update.message);
|
|
685
|
-
if (config
|
|
766
|
+
if (!isAllowedChat(config, inbound.chatId)) {
|
|
686
767
|
ignored += 1;
|
|
687
768
|
appendLog(config.paths.activityFile, `IGNORED chat=${inbound.chatId} message=${inbound.messageId}`);
|
|
688
769
|
continue;
|
|
@@ -692,10 +773,11 @@ export async function pollOnce() {
|
|
|
692
773
|
appendLog(config.paths.activityFile, `IGNORED_EMPTY chat=${inbound.chatId} message=${inbound.messageId}`);
|
|
693
774
|
continue;
|
|
694
775
|
}
|
|
776
|
+
inbound.relevance = classifyInboundRelevance(config, inbound);
|
|
695
777
|
state.queue.push(inbound);
|
|
696
778
|
state.lastInbound = inbound;
|
|
697
779
|
appendJsonl(config.paths.inboxFile, inbound);
|
|
698
|
-
appendLog(config.paths.activityFile, `IN chat=${inbound.chatId} message=${inbound.messageId} user=${inbound.user}: ${inbound.text.replace(/\s+/g, " ").slice(0, 180)}`);
|
|
780
|
+
appendLog(config.paths.activityFile, `IN chat=${inbound.chatId} message=${inbound.messageId} relevance=${inbound.relevance} user=${inbound.user}: ${inbound.text.replace(/\s+/g, " ").slice(0, 180)}`);
|
|
699
781
|
captured += 1;
|
|
700
782
|
}
|
|
701
783
|
|
|
@@ -717,9 +799,10 @@ export function listQueue(limit = 10) {
|
|
|
717
799
|
return state.queue.slice(-Math.max(1, limit));
|
|
718
800
|
}
|
|
719
801
|
|
|
720
|
-
export async function injectNext(threadId) {
|
|
802
|
+
export async function injectNext(threadId, options = {}) {
|
|
721
803
|
const config = loadConfig();
|
|
722
804
|
const state = loadState(config);
|
|
805
|
+
const auto = Boolean(options.auto);
|
|
723
806
|
const useAppServer = Boolean(config.appServerWsUrl);
|
|
724
807
|
const preferredThreadId = (
|
|
725
808
|
threadId
|
|
@@ -732,6 +815,53 @@ export async function injectNext(threadId) {
|
|
|
732
815
|
throw new Error("No bound thread id. Use bridge_bind_current_thread first.");
|
|
733
816
|
}
|
|
734
817
|
|
|
818
|
+
let next = null;
|
|
819
|
+
if (auto && String(config.dispatchMode || "deferred").toLowerCase() !== "legacy") {
|
|
820
|
+
next = state.queue.find((item) => {
|
|
821
|
+
if (item.status !== "queued") {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
if (String(item.chatType || "") === "private") {
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return ["direct", "lane", "escalation"].includes(String(item.relevance || ""));
|
|
828
|
+
});
|
|
829
|
+
} else {
|
|
830
|
+
next = state.queue.find((item) => item.status === "queued");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (!next) {
|
|
834
|
+
return {
|
|
835
|
+
ok: true,
|
|
836
|
+
status: auto ? "deferred" : "empty",
|
|
837
|
+
reason: auto ? "no_eligible_message" : undefined
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const bypassDeferredGate = auto && next.relevance === "escalation";
|
|
842
|
+
if (auto && !bypassDeferredGate && String(config.dispatchMode || "deferred").toLowerCase() !== "legacy") {
|
|
843
|
+
const openPendingReplies = countOpenPendingReplies(state);
|
|
844
|
+
if (openPendingReplies > 0) {
|
|
845
|
+
return {
|
|
846
|
+
ok: false,
|
|
847
|
+
status: "deferred",
|
|
848
|
+
reason: "pending_reply",
|
|
849
|
+
pendingReplies: openPendingReplies
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const sessionActivity = await resolveSessionActivity(config, resolvedThreadId);
|
|
854
|
+
if (sessionActivity.active) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
status: "deferred",
|
|
858
|
+
reason: "session_active",
|
|
859
|
+
quietMs: sessionActivity.quietMs,
|
|
860
|
+
readyInMs: Math.max(0, Number(config.idleCooldownMs || 0) - Number(sessionActivity.quietMs || 0))
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
735
865
|
let promoted = 0;
|
|
736
866
|
if (!useAppServer) {
|
|
737
867
|
for (const entry of state.queue) {
|
|
@@ -744,14 +874,6 @@ export async function injectNext(threadId) {
|
|
|
744
874
|
saveStateForConfig(config, state);
|
|
745
875
|
}
|
|
746
876
|
|
|
747
|
-
const next = state.queue.find((item) => item.status === "queued");
|
|
748
|
-
if (!next) {
|
|
749
|
-
return {
|
|
750
|
-
ok: true,
|
|
751
|
-
status: promoted > 0 ? "submitted" : "empty"
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
877
|
next.attempts = Number(next.attempts || 0) + 1;
|
|
756
878
|
next.lastAttemptAt = nowIso();
|
|
757
879
|
let sessionPath = "";
|
|
@@ -803,6 +925,7 @@ export async function injectNext(threadId) {
|
|
|
803
925
|
}
|
|
804
926
|
}
|
|
805
927
|
state.lastInjectAt = nowIso();
|
|
928
|
+
state.lastAutoDispatchAt = auto ? state.lastInjectAt : state.lastAutoDispatchAt;
|
|
806
929
|
const latestState = loadState(config);
|
|
807
930
|
saveStateForConfig(config, mergeStateSnapshots(latestState, state));
|
|
808
931
|
appendLog(config.paths.activityFile, `INJECT_${result.ok ? "OK" : "ERROR"} thread=${resolvedThreadId} message=${next.messageId}`);
|
|
@@ -18,6 +18,13 @@ function readDotEnvFile(path) {
|
|
|
18
18
|
return values;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function parseAllowedChatIds(rawValue) {
|
|
22
|
+
return String(rawValue || "")
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((value) => value.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
export function loadConfig() {
|
|
22
29
|
ensureStateLayout();
|
|
23
30
|
const paths = getPaths();
|
|
@@ -27,17 +34,21 @@ export function loadConfig() {
|
|
|
27
34
|
delete fallbackEnv.BLUN_TELEGRAM_AGENT_NAME;
|
|
28
35
|
delete fallbackEnv.BLUN_TELEGRAM_STATE_DIR;
|
|
29
36
|
delete fallbackEnv.BLUN_TELEGRAM_THREAD_ID;
|
|
30
|
-
const env = { ...fallbackEnv, ...
|
|
37
|
+
const env = { ...fallbackEnv, ...process.env, ...fileEnv };
|
|
38
|
+
const allowedChatIds = parseAllowedChatIds(env.BLUN_TELEGRAM_ALLOWED_CHAT_ID || env.TELEGRAM_ALLOWED_CHAT_ID || "");
|
|
31
39
|
return {
|
|
32
40
|
paths,
|
|
33
|
-
agentName: env.BLUN_TELEGRAM_AGENT_NAME?.trim() || "default",
|
|
41
|
+
agentName: env.BLUN_TELEGRAM_AGENT_NAME?.trim() || env.TELEGRAM_AGENT_NAME?.trim() || "default",
|
|
34
42
|
lane: env.BLUN_CODEX_LANE?.trim() || "",
|
|
35
|
-
botToken: env.BLUN_TELEGRAM_BOT_TOKEN?.trim() || "",
|
|
36
|
-
allowedChatId:
|
|
43
|
+
botToken: env.BLUN_TELEGRAM_BOT_TOKEN?.trim() || env.TELEGRAM_BOT_TOKEN?.trim() || "",
|
|
44
|
+
allowedChatId: allowedChatIds[0] || "",
|
|
45
|
+
allowedChatIds,
|
|
37
46
|
codexBin: env.BLUN_TELEGRAM_CODEX_BIN?.trim() || "codex",
|
|
38
47
|
appServerWsUrl: env.BLUN_TELEGRAM_APP_SERVER_WS_URL?.trim() || "",
|
|
39
48
|
currentThreadId: env.BLUN_TELEGRAM_THREAD_ID?.trim() || process.env.CODEX_THREAD_ID?.trim() || "",
|
|
40
49
|
resumeTimeoutMs: Number.parseInt(env.BLUN_TELEGRAM_RESUME_TIMEOUT_MS || "15000", 10) || 15000,
|
|
50
|
+
idleCooldownMs: Number.parseInt(env.BLUN_TELEGRAM_IDLE_COOLDOWN_MS || "15000", 10) || 15000,
|
|
51
|
+
dispatchMode: env.BLUN_TELEGRAM_DISPATCH_MODE?.trim() || "deferred",
|
|
41
52
|
pluginMode: env.BLUN_TELEGRAM_PLUGIN_MODE?.trim() || "inherit",
|
|
42
53
|
model: env.BLUN_CODEX_MODEL?.trim() || "",
|
|
43
54
|
reasoningEffort: env.BLUN_CODEX_REASONING_EFFORT?.trim() || "",
|
|
@@ -59,10 +59,12 @@ function ensureSidecar(scriptName, pidFile, stdoutFile, stderrFile, config, opti
|
|
|
59
59
|
BLUN_TELEGRAM_AGENT_NAME: config.agentName || "default",
|
|
60
60
|
BLUN_TELEGRAM_STATE_DIR: config.paths.root,
|
|
61
61
|
BLUN_TELEGRAM_BOT_TOKEN: config.botToken || "",
|
|
62
|
-
BLUN_TELEGRAM_ALLOWED_CHAT_ID: config.allowedChatId || "",
|
|
62
|
+
BLUN_TELEGRAM_ALLOWED_CHAT_ID: Array.isArray(config.allowedChatIds) ? config.allowedChatIds.join(",") : (config.allowedChatId || ""),
|
|
63
63
|
BLUN_TELEGRAM_APP_SERVER_WS_URL: config.appServerWsUrl || "",
|
|
64
64
|
BLUN_TELEGRAM_CODEX_BIN: config.codexBin || "codex",
|
|
65
65
|
BLUN_TELEGRAM_RESUME_TIMEOUT_MS: String(config.resumeTimeoutMs || 15000),
|
|
66
|
+
BLUN_TELEGRAM_IDLE_COOLDOWN_MS: String(config.idleCooldownMs || 15000),
|
|
67
|
+
BLUN_TELEGRAM_DISPATCH_MODE: config.dispatchMode || "deferred",
|
|
66
68
|
BLUN_TELEGRAM_PLUGIN_MODE: config.pluginMode || "plugin",
|
|
67
69
|
BLUN_CODEX_MODEL: config.model || "",
|
|
68
70
|
BLUN_CODEX_REASONING_EFFORT: config.reasoningEffort || "",
|
|
@@ -1,49 +1,50 @@
|
|
|
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
|
-
}
|
|
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
|
+
lastAutoDispatchAt: null
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string]$Profile = "default",
|
|
3
|
+
[switch]$EnsureConfigured,
|
|
4
|
+
[switch]$Json
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = "Stop"
|
|
8
|
+
|
|
9
|
+
function Try-ReadJson {
|
|
10
|
+
param([string]$Path)
|
|
11
|
+
if (-not (Test-Path $Path)) { return $null }
|
|
12
|
+
try { return Get-Content -Raw -Path $Path | ConvertFrom-Json } catch { return $null }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Ensure-Dir {
|
|
16
|
+
param([string]$Path)
|
|
17
|
+
if (-not (Test-Path $Path)) {
|
|
18
|
+
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Resolve-ConfiguredPath {
|
|
23
|
+
param([string]$Value, [string]$RuntimeRoot)
|
|
24
|
+
if (-not $Value) { return "" }
|
|
25
|
+
$expanded = [Environment]::ExpandEnvironmentVariables($Value)
|
|
26
|
+
if ([System.IO.Path]::IsPathRooted($expanded)) { return $expanded }
|
|
27
|
+
return [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot $expanded))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Read-DotEnvFile {
|
|
31
|
+
param([string]$Path)
|
|
32
|
+
$values = @{}
|
|
33
|
+
if (-not (Test-Path $Path)) { return $values }
|
|
34
|
+
foreach ($line in (Get-Content -Path $Path)) {
|
|
35
|
+
if (-not $line) { continue }
|
|
36
|
+
if ($line.Trim().StartsWith("#")) { continue }
|
|
37
|
+
$parts = $line -split "=", 2
|
|
38
|
+
if ($parts.Count -ne 2) { continue }
|
|
39
|
+
$values[$parts[0].Trim()] = $parts[1]
|
|
40
|
+
}
|
|
41
|
+
return $values
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Write-DotEnvFile {
|
|
45
|
+
param(
|
|
46
|
+
[string]$Path,
|
|
47
|
+
[hashtable]$Values
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
$lines = foreach ($key in ($Values.Keys | Sort-Object)) {
|
|
51
|
+
"$key=$($Values[$key])"
|
|
52
|
+
}
|
|
53
|
+
Set-Content -Path $Path -Value $lines -Encoding UTF8
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Test-TelegramTokenFormat {
|
|
57
|
+
param([string]$Value)
|
|
58
|
+
if (-not $Value) { return $false }
|
|
59
|
+
return $Value -match '^\d{6,}:[A-Za-z0-9_-]{20,}$'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Test-AllowedChatIdsFormat {
|
|
63
|
+
param([string]$Value)
|
|
64
|
+
if (-not $Value) { return $false }
|
|
65
|
+
$parts = @($Value -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
66
|
+
if ($parts.Count -eq 0) { return $false }
|
|
67
|
+
foreach ($part in $parts) {
|
|
68
|
+
if ($part -notmatch '^-?\d+$') {
|
|
69
|
+
return $false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return $true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Prompt-RequiredValue {
|
|
76
|
+
param(
|
|
77
|
+
[string]$Prompt,
|
|
78
|
+
[string]$CurrentValue,
|
|
79
|
+
[scriptblock]$Validator,
|
|
80
|
+
[string]$ErrorMessage
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
while ($true) {
|
|
84
|
+
$fullPrompt = if ($CurrentValue) { "$Prompt [$CurrentValue]" } else { $Prompt }
|
|
85
|
+
$inputValue = Read-Host -Prompt $fullPrompt
|
|
86
|
+
if (-not $inputValue -and $CurrentValue) {
|
|
87
|
+
$inputValue = $CurrentValue
|
|
88
|
+
}
|
|
89
|
+
$inputValue = [string]$inputValue
|
|
90
|
+
if (& $Validator $inputValue) {
|
|
91
|
+
return $inputValue
|
|
92
|
+
}
|
|
93
|
+
Write-Host $ErrorMessage -ForegroundColor Yellow
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Get-ProfilePath {
|
|
98
|
+
param(
|
|
99
|
+
[string]$RuntimeRoot,
|
|
100
|
+
[string]$ProfileName
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
$normalized = [string]$ProfileName
|
|
104
|
+
if (-not $normalized) { $normalized = "" }
|
|
105
|
+
$normalized = $normalized.ToLower()
|
|
106
|
+
$candidates = @()
|
|
107
|
+
if ($env:BLUN_CODEX_PROFILE_ROOT) {
|
|
108
|
+
$candidates += (Join-Path $env:BLUN_CODEX_PROFILE_ROOT ($normalized + ".json"))
|
|
109
|
+
}
|
|
110
|
+
$candidates += (Join-Path $env:USERPROFILE (".codex\\profiles\\codexlink\\" + $normalized + ".json"))
|
|
111
|
+
$candidates += (Join-Path $RuntimeRoot ("profiles\\" + $normalized + ".json"))
|
|
112
|
+
|
|
113
|
+
foreach ($candidate in $candidates) {
|
|
114
|
+
if ($candidate -and (Test-Path $candidate)) {
|
|
115
|
+
return $candidate
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return $candidates[-1]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
123
|
+
$profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Profile
|
|
124
|
+
$profileJson = Try-ReadJson -Path $profilePath
|
|
125
|
+
|
|
126
|
+
if (-not $profileJson) {
|
|
127
|
+
throw "Profile not found or invalid: $profilePath"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
$profileAgent = if ($profileJson.agent_name) { [string]$profileJson.agent_name } else { $Profile.ToLower() }
|
|
131
|
+
$stateDir = if ($profileJson.telegram -and $profileJson.telegram.state_dir) {
|
|
132
|
+
Resolve-ConfiguredPath -Value ([string]$profileJson.telegram.state_dir) -RuntimeRoot $runtimeRoot
|
|
133
|
+
} else {
|
|
134
|
+
Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profileAgent)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Ensure-Dir -Path $stateDir
|
|
138
|
+
$envPath = Join-Path $stateDir ".env"
|
|
139
|
+
$envValues = Read-DotEnvFile -Path $envPath
|
|
140
|
+
|
|
141
|
+
$currentToken = [string]$envValues["BLUN_TELEGRAM_BOT_TOKEN"]
|
|
142
|
+
if (-not (Test-TelegramTokenFormat -Value $currentToken)) {
|
|
143
|
+
$currentToken = [string]$envValues["TELEGRAM_BOT_TOKEN"]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
$currentAllowedChatIds = [string]$envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
|
|
147
|
+
if (-not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)) {
|
|
148
|
+
$currentAllowedChatIds = [string]$envValues["TELEGRAM_ALLOWED_CHAT_ID"]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$needsToken = -not (Test-TelegramTokenFormat -Value $currentToken)
|
|
152
|
+
$needsChatIds = -not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)
|
|
153
|
+
$changed = $false
|
|
154
|
+
|
|
155
|
+
if ($EnsureConfigured -and -not $needsToken -and -not $needsChatIds) {
|
|
156
|
+
$result = [ordered]@{
|
|
157
|
+
ok = $true
|
|
158
|
+
changed = $false
|
|
159
|
+
profile = $profileAgent
|
|
160
|
+
state_dir = $stateDir
|
|
161
|
+
env_path = $envPath
|
|
162
|
+
missing = @()
|
|
163
|
+
}
|
|
164
|
+
if ($Json) {
|
|
165
|
+
$result | ConvertTo-Json -Depth 6
|
|
166
|
+
} else {
|
|
167
|
+
Write-Host "Telegram ist bereits eingerichtet fuer Profil '$profileAgent'." -ForegroundColor Green
|
|
168
|
+
Write-Host "State-Ordner: $stateDir"
|
|
169
|
+
}
|
|
170
|
+
exit 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if ($EnsureConfigured -and $Json -and ($needsToken -or $needsChatIds)) {
|
|
174
|
+
$missing = @()
|
|
175
|
+
if ($needsToken) { $missing += "bot_token" }
|
|
176
|
+
if ($needsChatIds) { $missing += "allowed_chat_ids" }
|
|
177
|
+
[ordered]@{
|
|
178
|
+
ok = $false
|
|
179
|
+
changed = $false
|
|
180
|
+
profile = $profileAgent
|
|
181
|
+
state_dir = $stateDir
|
|
182
|
+
env_path = $envPath
|
|
183
|
+
missing = $missing
|
|
184
|
+
} | ConvertTo-Json -Depth 6
|
|
185
|
+
exit 2
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (-not $Json) {
|
|
189
|
+
Write-Host ""
|
|
190
|
+
Write-Host "CodexLink Telegram Setup" -ForegroundColor Cyan
|
|
191
|
+
Write-Host "Profil: $profileAgent"
|
|
192
|
+
Write-Host "Lokaler State-Ordner: $stateDir"
|
|
193
|
+
Write-Host ""
|
|
194
|
+
Write-Host "Ich speichere die Telegram-Werte automatisch an die richtige lokale Stelle." -ForegroundColor DarkGray
|
|
195
|
+
Write-Host "Du musst keine .env-Datei selbst suchen." -ForegroundColor DarkGray
|
|
196
|
+
Write-Host ""
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if ($needsToken) {
|
|
200
|
+
$currentToken = Prompt-RequiredValue `
|
|
201
|
+
-Prompt "Telegram Bot Token" `
|
|
202
|
+
-CurrentValue $currentToken `
|
|
203
|
+
-Validator { param($v) Test-TelegramTokenFormat -Value $v } `
|
|
204
|
+
-ErrorMessage "Bitte einen gueltigen Telegram Bot Token eingeben. Beispiel: 123456789:ABC..."
|
|
205
|
+
$envValues["BLUN_TELEGRAM_BOT_TOKEN"] = $currentToken
|
|
206
|
+
$changed = $true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if ($needsChatIds) {
|
|
210
|
+
$currentAllowedChatIds = Prompt-RequiredValue `
|
|
211
|
+
-Prompt "Erlaubte Chat ID(s), komma-getrennt" `
|
|
212
|
+
-CurrentValue $currentAllowedChatIds `
|
|
213
|
+
-Validator { param($v) Test-AllowedChatIdsFormat -Value $v } `
|
|
214
|
+
-ErrorMessage "Bitte mindestens eine numerische Chat-ID eingeben. Mehrere IDs mit Komma trennen."
|
|
215
|
+
$envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = (($currentAllowedChatIds -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -join ",")
|
|
216
|
+
$changed = $true
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
$envValues["BLUN_TELEGRAM_AGENT_NAME"] = $profileAgent
|
|
220
|
+
$envValues["BLUN_TELEGRAM_STATE_DIR"] = $stateDir
|
|
221
|
+
Write-DotEnvFile -Path $envPath -Values $envValues
|
|
222
|
+
|
|
223
|
+
$result = [ordered]@{
|
|
224
|
+
ok = $true
|
|
225
|
+
changed = $changed
|
|
226
|
+
profile = $profileAgent
|
|
227
|
+
state_dir = $stateDir
|
|
228
|
+
env_path = $envPath
|
|
229
|
+
missing = @()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ($Json) {
|
|
233
|
+
$result | ConvertTo-Json -Depth 6
|
|
234
|
+
exit 0
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
Write-Host ""
|
|
238
|
+
Write-Host "Telegram ist jetzt eingerichtet." -ForegroundColor Green
|
|
239
|
+
Write-Host "Gespeichert unter: $envPath"
|
|
240
|
+
Write-Host ""
|
|
241
|
+
Write-Host "Naechster Schritt:" -ForegroundColor Cyan
|
|
242
|
+
Write-Host " blun-codex --profile $profileAgent telegram-plugin"
|
|
243
|
+
Write-Host ""
|
|
244
|
+
Write-Host "Pruefen kannst du spaeter mit:" -ForegroundColor Cyan
|
|
245
|
+
Write-Host " blun-codex --profile $profileAgent telegram-doctor"
|