teleton 0.8.0 → 0.8.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.
Files changed (44) hide show
  1. package/README.md +28 -11
  2. package/dist/{chunk-H36RFKRI.js → chunk-2IZU3REP.js} +572 -174
  3. package/dist/chunk-3UFPFWYP.js +12 -0
  4. package/dist/{chunk-NUGDTPE4.js → chunk-4L66JHQE.js} +2 -1
  5. package/dist/{chunk-TVRZJIZX.js → chunk-55SKE6YH.js} +4 -4
  6. package/dist/{setup-server-QXED3D2L.js → chunk-57URFK6M.js} +161 -210
  7. package/dist/chunk-5SEMA47R.js +75 -0
  8. package/dist/{chunk-JHYZYFZJ.js → chunk-7YKSXOQQ.js} +17 -2
  9. package/dist/{chunk-IJBWWQE4.js → chunk-C4NKJT2Z.js} +12 -0
  10. package/dist/{chunk-RQBAMUCV.js → chunk-GGXJLMOH.js} +1451 -743
  11. package/dist/{chunk-WIKM24GZ.js → chunk-H7MFXJZK.js} +7 -2
  12. package/dist/{chunk-U56QTM46.js → chunk-HEDJCLA6.js} +85 -44
  13. package/dist/{chunk-QVBSUYVX.js → chunk-J73TA3UM.js} +17 -9
  14. package/dist/{chunk-P36I6OIV.js → chunk-LC4TV3KL.js} +13 -2
  15. package/dist/{chunk-RCMD3U65.js → chunk-NQ6FZKCE.js} +13 -0
  16. package/dist/{chunk-SD4NLLYG.js → chunk-VYKW7FMV.js} +224 -93
  17. package/dist/chunk-W25Z7CM6.js +487 -0
  18. package/dist/{chunk-OJCLKU5Z.js → chunk-WFTC3JJW.js} +16 -0
  19. package/dist/{server-H3QA252W.js → chunk-XBSCYMKM.js} +369 -374
  20. package/dist/{chunk-PHSAHTK4.js → chunk-YOSUPUAJ.js} +75 -7
  21. package/dist/cli/index.js +67 -22
  22. package/dist/{client-LNZTDQSA.js → client-YOOHI776.js} +4 -4
  23. package/dist/{get-my-gifts-OMGKOEPM.js → get-my-gifts-Y7EN7RK4.js} +3 -3
  24. package/dist/index.js +15 -14
  25. package/dist/{memory-AS7WKGTW.js → memory-Q6EWGK2S.js} +7 -5
  26. package/dist/memory-hook-WUXJNVT5.js +18 -0
  27. package/dist/{migrate-POHWYEIW.js → migrate-WFU6COBN.js} +5 -5
  28. package/dist/server-GYZXKIKU.js +787 -0
  29. package/dist/server-YODFBZKG.js +392 -0
  30. package/dist/setup-server-IZBUOJRU.js +215 -0
  31. package/dist/{store-GAFULOOX.js → store-7M4XV6M5.js} +6 -6
  32. package/dist/{task-dependency-resolver-3FIKQ7Z6.js → task-dependency-resolver-L6UUMTHK.js} +3 -3
  33. package/dist/{task-executor-RUTFG6VG.js → task-executor-XBNJLUCS.js} +3 -3
  34. package/dist/{tasks-BEZ4QRI2.js → tasks-WQIKXDX5.js} +1 -1
  35. package/dist/{tool-adapter-IH5VGBOO.js → tool-adapter-IVX2XQJE.js} +1 -1
  36. package/dist/{tool-index-H3SHOJC3.js → tool-index-NYH57UWP.js} +9 -6
  37. package/dist/{transcript-IMNE6KU3.js → transcript-IM7G25OS.js} +2 -2
  38. package/dist/web/assets/index-BfYCdwLI.js +80 -0
  39. package/dist/web/assets/{index-BrVqauzj.css → index-DmlyQVhR.css} +1 -1
  40. package/dist/web/assets/{index.es-DkU1GvWU.js → index.es-DitvF-9H.js} +1 -1
  41. package/dist/web/index.html +2 -2
  42. package/package.json +14 -5
  43. package/dist/chunk-XBE4JB7C.js +0 -8
  44. package/dist/web/assets/index-DYeEkvJ6.js +0 -72
@@ -7,7 +7,7 @@ import {
7
7
  getWalletBalance,
8
8
  invalidateTonClientCache,
9
9
  loadWallet
10
- } from "./chunk-JHYZYFZJ.js";
10
+ } from "./chunk-7YKSXOQQ.js";
11
11
  import {
12
12
  getOrCreateSession,
13
13
  getSession,
@@ -15,7 +15,15 @@ import {
15
15
  resetSessionWithPolicy,
16
16
  shouldResetSession,
17
17
  updateSession
18
- } from "./chunk-TVRZJIZX.js";
18
+ } from "./chunk-55SKE6YH.js";
19
+ import {
20
+ saveSessionMemory,
21
+ summarizeWithFallback
22
+ } from "./chunk-W25Z7CM6.js";
23
+ import {
24
+ getErrorMessage,
25
+ isHttpError
26
+ } from "./chunk-3UFPFWYP.js";
19
27
  import {
20
28
  Mi,
21
29
  Mn,
@@ -26,41 +34,33 @@ import {
26
34
  ut,
27
35
  ye
28
36
  } from "./chunk-7TECSLJ4.js";
29
- import {
30
- getErrorMessage
31
- } from "./chunk-XBE4JB7C.js";
32
37
  import {
33
38
  chatWithContext,
34
39
  getEffectiveApiKey,
35
40
  getProviderModel,
36
- getUtilityModel,
37
41
  loadContextFromTranscript
38
- } from "./chunk-QVBSUYVX.js";
42
+ } from "./chunk-J73TA3UM.js";
39
43
  import {
40
44
  getProviderMetadata,
41
45
  getSupportedProviders
42
- } from "./chunk-PHSAHTK4.js";
46
+ } from "./chunk-YOSUPUAJ.js";
43
47
  import {
44
48
  appendToTranscript,
45
49
  archiveTranscript,
46
50
  transcriptExists
47
- } from "./chunk-P36I6OIV.js";
51
+ } from "./chunk-LC4TV3KL.js";
48
52
  import {
49
53
  ContextBuilder,
50
54
  createDbWrapper,
51
55
  getDatabase,
52
56
  migrateFromMainDb,
53
57
  openModuleDb
54
- } from "./chunk-SD4NLLYG.js";
58
+ } from "./chunk-VYKW7FMV.js";
55
59
  import {
56
60
  GECKOTERMINAL_API_URL,
57
61
  tonapiFetch
58
62
  } from "./chunk-VFA7QMCZ.js";
59
63
  import {
60
- ADAPTIVE_CHUNK_RATIO_BASE,
61
- ADAPTIVE_CHUNK_RATIO_MIN,
62
- ADAPTIVE_CHUNK_RATIO_TRIGGER,
63
- CHARS_PER_TOKEN_ESTIMATE,
64
64
  COMPACTION_KEEP_RECENT,
65
65
  COMPACTION_MAX_MESSAGES,
66
66
  COMPACTION_MAX_TOKENS_RATIO,
@@ -72,19 +72,18 @@ import {
72
72
  DEFAULT_MAX_SUMMARY_TOKENS,
73
73
  DEFAULT_MAX_TOKENS,
74
74
  DEFAULT_SOFT_THRESHOLD_TOKENS,
75
- DEFAULT_SUMMARY_FALLBACK_TOKENS,
75
+ EMBEDDING_QUERY_MAX_CHARS,
76
76
  FALLBACK_SOFT_THRESHOLD_TOKENS,
77
77
  MASKING_KEEP_RECENT_COUNT,
78
78
  MAX_TOOL_RESULT_SIZE,
79
79
  MEMORY_FLUSH_RECENT_MESSAGES,
80
- OVERSIZED_MESSAGE_RATIO,
81
80
  PAYMENT_TOLERANCE_RATIO,
82
81
  RATE_LIMIT_MAX_RETRIES,
82
+ RESULT_TRUNCATION_KEEP_CHARS,
83
+ RESULT_TRUNCATION_THRESHOLD,
83
84
  SERVER_ERROR_MAX_RETRIES,
84
- SESSION_SLUG_MAX_TOKENS,
85
- SESSION_SLUG_RECENT_MESSAGES,
86
- TOKEN_ESTIMATE_SAFETY_MARGIN
87
- } from "./chunk-IJBWWQE4.js";
85
+ TOOL_CONCURRENCY_LIMIT
86
+ } from "./chunk-C4NKJT2Z.js";
88
87
  import {
89
88
  fetchWithTimeout
90
89
  } from "./chunk-XQUHC3JZ.js";
@@ -105,7 +104,7 @@ import {
105
104
  } from "./chunk-EYWNOHMJ.js";
106
105
  import {
107
106
  createLogger
108
- } from "./chunk-RCMD3U65.js";
107
+ } from "./chunk-NQ6FZKCE.js";
109
108
 
110
109
  // src/config/loader.ts
111
110
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
@@ -191,6 +190,17 @@ Run 'teleton setup' to create one.`);
191
190
  );
192
191
  }
193
192
  }
193
+ if (process.env.TELETON_API_ENABLED) {
194
+ if (!config.api) config.api = { enabled: false, port: 7778, key_hash: "", allowed_ips: [] };
195
+ config.api.enabled = process.env.TELETON_API_ENABLED === "true";
196
+ }
197
+ if (process.env.TELETON_API_PORT) {
198
+ const port = parseInt(process.env.TELETON_API_PORT, 10);
199
+ if (!isNaN(port) && port >= 1024 && port <= 65535) {
200
+ if (!config.api) config.api = { enabled: false, port: 7778, key_hash: "", allowed_ips: [] };
201
+ config.api.port = port;
202
+ }
203
+ }
194
204
  if (process.env.TELETON_BASE_URL) {
195
205
  try {
196
206
  new URL(process.env.TELETON_BASE_URL);
@@ -364,7 +374,7 @@ function appendToDailyLog(content, date = /* @__PURE__ */ new Date()) {
364
374
  const header = `# Daily Log - ${formatDate(date)}
365
375
 
366
376
  `;
367
- appendFileSync(logPath, header, "utf-8");
377
+ appendFileSync(logPath, header, { encoding: "utf-8", mode: 384 });
368
378
  }
369
379
  const entry = `## ${timestamp}
370
380
 
@@ -739,398 +749,6 @@ ${body}`;
739
749
 
740
750
  // src/memory/compaction.ts
741
751
  import { randomUUID } from "crypto";
742
-
743
- // src/memory/ai-summarization.ts
744
- import {
745
- complete
746
- } from "@mariozechner/pi-ai";
747
- var log3 = createLogger("Memory");
748
- function estimateMessageTokens(content) {
749
- return Math.ceil(content.length / CHARS_PER_TOKEN_ESTIMATE * TOKEN_ESTIMATE_SAFETY_MARGIN);
750
- }
751
- function splitMessagesByTokens(messages, maxChunkTokens) {
752
- if (messages.length === 0) {
753
- return [];
754
- }
755
- const chunks = [];
756
- let currentChunk = [];
757
- let currentTokens = 0;
758
- for (const message of messages) {
759
- const content = extractMessageContent(message);
760
- const messageTokens = estimateMessageTokens(content);
761
- if (currentChunk.length > 0 && currentTokens + messageTokens > maxChunkTokens) {
762
- chunks.push(currentChunk);
763
- currentChunk = [];
764
- currentTokens = 0;
765
- }
766
- currentChunk.push(message);
767
- currentTokens += messageTokens;
768
- if (messageTokens > maxChunkTokens && currentChunk.length === 1) {
769
- chunks.push(currentChunk);
770
- currentChunk = [];
771
- currentTokens = 0;
772
- }
773
- }
774
- if (currentChunk.length > 0) {
775
- chunks.push(currentChunk);
776
- }
777
- return chunks;
778
- }
779
- function extractMessageContent(message) {
780
- if (message.role === "user") {
781
- return typeof message.content === "string" ? message.content : "[complex content]";
782
- } else if (message.role === "assistant") {
783
- return message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
784
- }
785
- return "";
786
- }
787
- function formatMessagesForSummary(messages) {
788
- const formatted = [];
789
- for (const msg of messages) {
790
- if (msg.role === "user") {
791
- const content = typeof msg.content === "string" ? msg.content : "[complex]";
792
- const bodyMatch = content.match(/\] (.+)/s);
793
- const body = bodyMatch ? bodyMatch[1] : content;
794
- formatted.push(`User: ${body}`);
795
- } else if (msg.role === "assistant") {
796
- const textBlocks = msg.content.filter((b) => b.type === "text");
797
- if (textBlocks.length > 0) {
798
- const text = textBlocks.map((b) => b.text).join("\n");
799
- formatted.push(`Assistant: ${text}`);
800
- }
801
- const toolCalls = msg.content.filter((b) => b.type === "toolCall");
802
- if (toolCalls.length > 0) {
803
- const toolNames = toolCalls.map((b) => b.name).join(", ");
804
- formatted.push(`[Used tools: ${toolNames}]`);
805
- }
806
- } else if (msg.role === "toolResult") {
807
- formatted.push(`[Tool result: ${msg.toolName}]`);
808
- }
809
- }
810
- return formatted.join("\n\n");
811
- }
812
- function isOversizedForSummary(message, contextWindow) {
813
- const content = extractMessageContent(message);
814
- const tokens = estimateMessageTokens(content);
815
- return tokens > contextWindow * OVERSIZED_MESSAGE_RATIO;
816
- }
817
- function computeAdaptiveChunkRatio(messages, contextWindow) {
818
- const BASE_CHUNK_RATIO = ADAPTIVE_CHUNK_RATIO_BASE;
819
- const MIN_CHUNK_RATIO = ADAPTIVE_CHUNK_RATIO_MIN;
820
- if (messages.length === 0) {
821
- return BASE_CHUNK_RATIO;
822
- }
823
- let totalTokens = 0;
824
- for (const msg of messages) {
825
- const content = extractMessageContent(msg);
826
- totalTokens += estimateMessageTokens(content);
827
- }
828
- const avgTokens = totalTokens / messages.length;
829
- const avgRatio = avgTokens / contextWindow;
830
- if (avgRatio > ADAPTIVE_CHUNK_RATIO_TRIGGER) {
831
- const reduction = Math.min(avgRatio * 2, BASE_CHUNK_RATIO - MIN_CHUNK_RATIO);
832
- return Math.max(MIN_CHUNK_RATIO, BASE_CHUNK_RATIO - reduction);
833
- }
834
- return BASE_CHUNK_RATIO;
835
- }
836
- async function summarizeViaClaude(params) {
837
- const provider = params.provider || "anthropic";
838
- const model = getUtilityModel(provider, params.utilityModel);
839
- const maxTokens = params.maxSummaryTokens ?? DEFAULT_SUMMARY_FALLBACK_TOKENS;
840
- const formatted = formatMessagesForSummary(params.messages);
841
- if (!formatted.trim()) {
842
- return "No conversation content to summarize.";
843
- }
844
- const defaultInstructions = `Summarize this conversation concisely. Focus on:
845
- - Key decisions made
846
- - Action items and TODOs
847
- - Open questions
848
- - Important context and constraints
849
- - Technical details that matter
850
-
851
- Be specific but concise. Preserve critical information.`;
852
- const instructions = params.customInstructions ? `${defaultInstructions}
853
-
854
- Additional focus:
855
- ${params.customInstructions}` : defaultInstructions;
856
- try {
857
- const context = {
858
- messages: [
859
- {
860
- role: "user",
861
- content: `${instructions}
862
-
863
- Conversation:
864
- ${formatted}`,
865
- timestamp: Date.now()
866
- }
867
- ]
868
- };
869
- const response = await complete(model, context, {
870
- apiKey: params.apiKey,
871
- maxTokens
872
- });
873
- const textContent = response.content.find((block) => block.type === "text");
874
- const summary = textContent?.type === "text" ? textContent.text : "";
875
- return summary.trim() || "Unable to generate summary.";
876
- } catch (error) {
877
- log3.error({ err: error }, "Summarization error");
878
- throw new Error(`Summarization failed: ${getErrorMessage(error)}`);
879
- }
880
- }
881
- async function summarizeInChunks(params) {
882
- if (params.messages.length === 0) {
883
- return {
884
- summary: "No messages to summarize.",
885
- tokensUsed: 0,
886
- chunksProcessed: 0
887
- };
888
- }
889
- const chunks = splitMessagesByTokens(params.messages, params.maxChunkTokens);
890
- log3.info(`Splitting into ${chunks.length} chunks for summarization`);
891
- if (chunks.length === 1) {
892
- const summary = await summarizeViaClaude({
893
- messages: chunks[0],
894
- apiKey: params.apiKey,
895
- maxSummaryTokens: params.maxSummaryTokens,
896
- customInstructions: params.customInstructions,
897
- provider: params.provider,
898
- utilityModel: params.utilityModel
899
- });
900
- return {
901
- summary,
902
- tokensUsed: estimateMessageTokens(summary),
903
- chunksProcessed: 1
904
- };
905
- }
906
- const partialSummaries = [];
907
- for (let i = 0; i < chunks.length; i++) {
908
- log3.info(`Summarizing chunk ${i + 1}/${chunks.length} (${chunks[i].length} messages)`);
909
- const partial = await summarizeViaClaude({
910
- messages: chunks[i],
911
- apiKey: params.apiKey,
912
- maxSummaryTokens: Math.floor(
913
- (params.maxSummaryTokens ?? DEFAULT_SUMMARY_FALLBACK_TOKENS) / 2
914
- ),
915
- customInstructions: params.customInstructions,
916
- provider: params.provider,
917
- utilityModel: params.utilityModel
918
- });
919
- partialSummaries.push(partial);
920
- }
921
- log3.info(`Merging ${partialSummaries.length} partial summaries`);
922
- const provider = params.provider || "anthropic";
923
- const model = getUtilityModel(provider, params.utilityModel);
924
- const mergeContext = {
925
- messages: [
926
- {
927
- role: "user",
928
- content: `Merge these partial conversation summaries into one cohesive summary.
929
- Preserve all key decisions, action items, open questions, and important context.
930
- Do not add new information - only synthesize what's provided.
931
-
932
- Partial summaries:
933
-
934
- ${partialSummaries.map((s, i) => `Part ${i + 1}:
935
- ${s}`).join("\n\n---\n\n")}`,
936
- timestamp: Date.now()
937
- }
938
- ]
939
- };
940
- const mergeResponse = await complete(model, mergeContext, {
941
- apiKey: params.apiKey,
942
- maxTokens: params.maxSummaryTokens ?? DEFAULT_SUMMARY_FALLBACK_TOKENS
943
- });
944
- const textContent = mergeResponse.content.find((block) => block.type === "text");
945
- const merged = textContent?.type === "text" ? textContent.text : "";
946
- return {
947
- summary: merged.trim() || "Unable to merge summaries.",
948
- tokensUsed: estimateMessageTokens(merged),
949
- chunksProcessed: chunks.length
950
- };
951
- }
952
- async function summarizeWithFallback(params) {
953
- if (params.messages.length === 0) {
954
- return {
955
- summary: "No messages to summarize.",
956
- tokensUsed: 0,
957
- chunksProcessed: 0
958
- };
959
- }
960
- const chunkRatio = computeAdaptiveChunkRatio(params.messages, params.contextWindow);
961
- const maxChunkTokens = Math.floor(params.contextWindow * chunkRatio);
962
- log3.info(
963
- `AI Summarization: ${params.messages.length} messages, chunk ratio: ${(chunkRatio * 100).toFixed(0)}%`
964
- );
965
- try {
966
- return await summarizeInChunks({
967
- messages: params.messages,
968
- apiKey: params.apiKey,
969
- maxChunkTokens,
970
- maxSummaryTokens: params.maxSummaryTokens,
971
- customInstructions: params.customInstructions,
972
- provider: params.provider,
973
- utilityModel: params.utilityModel
974
- });
975
- } catch (fullError) {
976
- log3.warn(
977
- `Full summarization failed: ${fullError instanceof Error ? fullError.message : String(fullError)}`
978
- );
979
- }
980
- const smallMessages = [];
981
- const oversizedNotes = [];
982
- for (const msg of params.messages) {
983
- if (isOversizedForSummary(msg, params.contextWindow)) {
984
- const content = extractMessageContent(msg);
985
- const tokens = estimateMessageTokens(content);
986
- oversizedNotes.push(
987
- `[Large ${msg.role} message (~${Math.round(tokens / 1e3)}K tokens) omitted from summary]`
988
- );
989
- } else {
990
- smallMessages.push(msg);
991
- }
992
- }
993
- log3.info(
994
- `Fallback: Processing ${smallMessages.length} messages, skipping ${oversizedNotes.length} oversized`
995
- );
996
- if (smallMessages.length > 0) {
997
- try {
998
- const result = await summarizeInChunks({
999
- messages: smallMessages,
1000
- apiKey: params.apiKey,
1001
- maxChunkTokens,
1002
- maxSummaryTokens: params.maxSummaryTokens,
1003
- customInstructions: params.customInstructions,
1004
- provider: params.provider,
1005
- utilityModel: params.utilityModel
1006
- });
1007
- const notes = oversizedNotes.length > 0 ? `
1008
-
1009
- ${oversizedNotes.join("\n")}` : "";
1010
- return {
1011
- summary: result.summary + notes,
1012
- tokensUsed: result.tokensUsed,
1013
- chunksProcessed: result.chunksProcessed
1014
- };
1015
- } catch (partialError) {
1016
- log3.warn(
1017
- `Partial summarization also failed: ${partialError instanceof Error ? partialError.message : String(partialError)}`
1018
- );
1019
- }
1020
- }
1021
- const note = `Context contained ${params.messages.length} messages (${oversizedNotes.length} were oversized). AI summarization unavailable due to size constraints. Recent conversation history was preserved.`;
1022
- return {
1023
- summary: note,
1024
- tokensUsed: estimateMessageTokens(note),
1025
- chunksProcessed: 0
1026
- };
1027
- }
1028
-
1029
- // src/session/memory-hook.ts
1030
- import { writeFile, mkdir } from "fs/promises";
1031
- import { join as join3 } from "path";
1032
- import { complete as complete2 } from "@mariozechner/pi-ai";
1033
- var log4 = createLogger("Session");
1034
- async function generateSlugViaClaude(params) {
1035
- const provider = params.provider || "anthropic";
1036
- const model = getUtilityModel(provider, params.utilityModel);
1037
- const formatted = formatMessagesForSummary(params.messages.slice(-SESSION_SLUG_RECENT_MESSAGES));
1038
- if (!formatted.trim()) {
1039
- return "empty-session";
1040
- }
1041
- try {
1042
- const context = {
1043
- messages: [
1044
- {
1045
- role: "user",
1046
- content: `Generate a short, descriptive slug (2-4 words, kebab-case) for this conversation.
1047
- Examples: "gift-transfer-fix", "context-overflow-debug", "telegram-integration"
1048
-
1049
- Conversation:
1050
- ${formatted}
1051
-
1052
- Slug:`,
1053
- timestamp: Date.now()
1054
- }
1055
- ]
1056
- };
1057
- const response = await complete2(model, context, {
1058
- apiKey: params.apiKey,
1059
- maxTokens: SESSION_SLUG_MAX_TOKENS
1060
- });
1061
- const textContent = response.content.find((block) => block.type === "text");
1062
- const slug = textContent?.type === "text" ? textContent.text.trim() : "";
1063
- return slug.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50) || "session";
1064
- } catch (error) {
1065
- log4.warn({ err: error }, "Slug generation failed, using fallback");
1066
- const now = /* @__PURE__ */ new Date();
1067
- return `session-${now.getHours().toString().padStart(2, "0")}${now.getMinutes().toString().padStart(2, "0")}`;
1068
- }
1069
- }
1070
- async function saveSessionMemory(params) {
1071
- try {
1072
- const { TELETON_ROOT: TELETON_ROOT2 } = await import("./paths-XA2RJH4S.js");
1073
- const memoryDir = join3(TELETON_ROOT2, "memory");
1074
- await mkdir(memoryDir, { recursive: true });
1075
- const now = /* @__PURE__ */ new Date();
1076
- const dateStr = now.toISOString().split("T")[0];
1077
- log4.info("Generating semantic slug for session memory...");
1078
- const slug = await generateSlugViaClaude({
1079
- messages: params.context.messages,
1080
- apiKey: params.apiKey,
1081
- provider: params.provider,
1082
- utilityModel: params.utilityModel
1083
- });
1084
- const filename = `${dateStr}-${slug}.md`;
1085
- const filepath = join3(memoryDir, filename);
1086
- const timeStr = now.toISOString().split("T")[1].split(".")[0];
1087
- log4.info("Generating session summary...");
1088
- let summary;
1089
- try {
1090
- summary = await summarizeViaClaude({
1091
- messages: params.context.messages,
1092
- apiKey: params.apiKey,
1093
- maxSummaryTokens: DEFAULT_MAX_SUMMARY_TOKENS,
1094
- customInstructions: "Summarize this session comprehensively. Include key topics, decisions made, problems solved, and important context.",
1095
- provider: params.provider,
1096
- utilityModel: params.utilityModel
1097
- });
1098
- } catch (error) {
1099
- log4.warn({ err: error }, "Session summary generation failed");
1100
- summary = `Session contained ${params.context.messages.length} messages. Summary generation failed.`;
1101
- }
1102
- const content = `# Session Memory: ${dateStr} ${timeStr} UTC
1103
-
1104
- ## Metadata
1105
-
1106
- - **Old Session ID**: \`${params.oldSessionId}\`
1107
- - **New Session ID**: \`${params.newSessionId}\`
1108
- - **Chat ID**: \`${params.chatId}\`
1109
- - **Timestamp**: ${now.toISOString()}
1110
- - **Message Count**: ${params.context.messages.length}
1111
-
1112
- ## Session Summary
1113
-
1114
- ${summary}
1115
-
1116
- ## Context
1117
-
1118
- This session was compacted and migrated to a new session ID. The summary above preserves key information for continuity.
1119
-
1120
- ---
1121
-
1122
- *Generated automatically by Teleton-AI session memory hook*
1123
- `;
1124
- await writeFile(filepath, content, "utf-8");
1125
- const relPath = filepath.replace(TELETON_ROOT2, "~/.teleton");
1126
- log4.info(`Session memory saved: ${relPath}`);
1127
- } catch (error) {
1128
- log4.error({ err: error }, "Failed to save session memory");
1129
- }
1130
- }
1131
-
1132
- // src/memory/compaction.ts
1133
- import { encodingForModel } from "js-tiktoken";
1134
752
  var DEFAULT_COMPACTION_CONFIG = {
1135
753
  enabled: true,
1136
754
  maxMessages: COMPACTION_MAX_MESSAGES,
@@ -1139,7 +757,7 @@ var DEFAULT_COMPACTION_CONFIG = {
1139
757
  memoryFlushEnabled: true,
1140
758
  softThresholdTokens: DEFAULT_SOFT_THRESHOLD_TOKENS
1141
759
  };
1142
- var log5 = createLogger("Memory");
760
+ var log3 = createLogger("Memory");
1143
761
  function estimateContextTokens(context) {
1144
762
  let charCount = 0;
1145
763
  if (context.systemPrompt) {
@@ -1171,7 +789,7 @@ function shouldFlushMemory(context, config, tokenCount) {
1171
789
  const tokens = tokenCount ?? estimateContextTokens(context);
1172
790
  const softThreshold = config.softThresholdTokens ?? FALLBACK_SOFT_THRESHOLD_TOKENS;
1173
791
  if (tokens >= softThreshold) {
1174
- log5.info(`Memory flush needed: ~${tokens} tokens (soft threshold: ${softThreshold})`);
792
+ log3.info(`Memory flush needed: ~${tokens} tokens (soft threshold: ${softThreshold})`);
1175
793
  return true;
1176
794
  }
1177
795
  return false;
@@ -1193,7 +811,7 @@ function flushMemoryToDailyLog(context) {
1193
811
  }
1194
812
  }
1195
813
  writeSummaryToDailyLog(summary.join("\n"));
1196
- log5.info(`Memory flushed to daily log`);
814
+ log3.info(`Memory flushed to daily log`);
1197
815
  }
1198
816
  function shouldCompact(context, config, tokenCount) {
1199
817
  if (!config.enabled) {
@@ -1201,13 +819,13 @@ function shouldCompact(context, config, tokenCount) {
1201
819
  }
1202
820
  const messageCount = context.messages.length;
1203
821
  if (config.maxMessages && messageCount >= config.maxMessages) {
1204
- log5.info(`Compaction needed: ${messageCount} messages (max: ${config.maxMessages})`);
822
+ log3.info(`Compaction needed: ${messageCount} messages (max: ${config.maxMessages})`);
1205
823
  return true;
1206
824
  }
1207
825
  if (config.maxTokens) {
1208
826
  const tokens = tokenCount ?? estimateContextTokens(context);
1209
827
  if (tokens >= config.maxTokens) {
1210
- log5.info(`Compaction needed: ~${tokens} tokens (max: ${config.maxTokens})`);
828
+ log3.info(`Compaction needed: ~${tokens} tokens (max: ${config.maxTokens})`);
1211
829
  return true;
1212
830
  }
1213
831
  }
@@ -1253,12 +871,12 @@ async function compactContext(context, config, apiKey, provider, utilityModel) {
1253
871
  iterations++;
1254
872
  }
1255
873
  if (hasOrphanedToolResults(context.messages.slice(cutIndex))) {
1256
- log5.warn(`Compaction: couldn't find clean cut point, keeping all messages`);
874
+ log3.warn(`Compaction: couldn't find clean cut point, keeping all messages`);
1257
875
  return context;
1258
876
  }
1259
877
  const recentMessages = context.messages.slice(cutIndex);
1260
878
  const oldMessages = context.messages.slice(0, cutIndex);
1261
- log5.info(
879
+ log3.info(
1262
880
  `Compacting ${oldMessages.length} old messages, keeping ${recentMessages.length} recent (cut at clean boundary)`
1263
881
  );
1264
882
  try {
@@ -1288,7 +906,7 @@ Keep each section concise. Omit a section if empty. Preserve specific names, num
1288
906
  provider,
1289
907
  utilityModel
1290
908
  });
1291
- log5.info(`AI Summary: ${result.tokensUsed} tokens, ${result.chunksProcessed} chunks processed`);
909
+ log3.info(`AI Summary: ${result.tokensUsed} tokens, ${result.chunksProcessed} chunks processed`);
1292
910
  const summaryText = `[Auto-compacted ${oldMessages.length} messages]
1293
911
 
1294
912
  ${result.summary}`;
@@ -1302,7 +920,7 @@ ${result.summary}`;
1302
920
  messages: [summaryMessage, ...recentMessages]
1303
921
  };
1304
922
  } catch (error) {
1305
- log5.error({ err: error }, "AI summarization failed, using fallback");
923
+ log3.error({ err: error }, "AI summarization failed, using fallback");
1306
924
  const summaryText = `[Auto-compacted: ${oldMessages.length} earlier messages from this conversation]`;
1307
925
  const summaryMessage = {
1308
926
  role: "user",
@@ -1317,7 +935,7 @@ ${result.summary}`;
1317
935
  }
1318
936
  async function compactAndSaveTranscript(sessionId, context, config, apiKey, chatId, provider, utilityModel) {
1319
937
  const newSessionId = randomUUID();
1320
- log5.info(`Creating compacted transcript: ${sessionId} \u2192 ${newSessionId}`);
938
+ log3.info(`Creating compacted transcript: ${sessionId} \u2192 ${newSessionId}`);
1321
939
  if (chatId) {
1322
940
  await saveSessionMemory({
1323
941
  oldSessionId: sessionId,
@@ -1351,7 +969,7 @@ var CompactionManager = class {
1351
969
  if (this.config.memoryFlushEnabled) {
1352
970
  flushMemoryToDailyLog(context);
1353
971
  }
1354
- log5.info(`Auto-compacting session ${sessionId}`);
972
+ log3.info(`Auto-compacting session ${sessionId}`);
1355
973
  const newSessionId = await compactAndSaveTranscript(
1356
974
  sessionId,
1357
975
  context,
@@ -1361,7 +979,7 @@ var CompactionManager = class {
1361
979
  provider,
1362
980
  utilityModel
1363
981
  );
1364
- log5.info(`Compaction complete: ${newSessionId}`);
982
+ log3.info(`Compaction complete: ${newSessionId}`);
1365
983
  return newSessionId;
1366
984
  }
1367
985
  updateConfig(config) {
@@ -1375,62 +993,125 @@ var CompactionManager = class {
1375
993
  // src/memory/observation-masking.ts
1376
994
  var DEFAULT_MASKING_CONFIG = {
1377
995
  keepRecentCount: MASKING_KEEP_RECENT_COUNT,
1378
- keepErrorResults: true
996
+ keepErrorResults: true,
997
+ truncationThreshold: RESULT_TRUNCATION_THRESHOLD,
998
+ truncationKeepChars: RESULT_TRUNCATION_KEEP_CHARS
1379
999
  };
1380
1000
  var isCocoonToolResult = (msg) => msg.role === "user" && Array.isArray(msg.content) && msg.content.some((c2) => c2.type === "text" && c2.text.includes("<tool_response>"));
1381
- function maskOldToolResults(messages, config = DEFAULT_MASKING_CONFIG, toolRegistry) {
1001
+ function isExempt(toolMsg, config, toolRegistry) {
1002
+ if (config.keepErrorResults && toolMsg.isError) return true;
1003
+ if (toolRegistry && toolRegistry.getToolCategory(toolMsg.toolName) === "data-bearing")
1004
+ return true;
1005
+ return false;
1006
+ }
1007
+ function truncateToolResult(text, keepChars) {
1008
+ try {
1009
+ const parsed = JSON.parse(text);
1010
+ if (parsed.data?.summary) {
1011
+ return JSON.stringify({
1012
+ success: parsed.success,
1013
+ data: { summary: parsed.data.summary, _truncated: true }
1014
+ });
1015
+ }
1016
+ if (parsed.data?.message) {
1017
+ return JSON.stringify({
1018
+ success: parsed.success,
1019
+ data: { summary: parsed.data.message, _truncated: true }
1020
+ });
1021
+ }
1022
+ } catch {
1023
+ }
1024
+ return text.slice(0, keepChars) + `
1025
+ ...[truncated, original: ${text.length} chars]`;
1026
+ }
1027
+ function maskOldToolResults(messages, options) {
1028
+ const config = options?.config ?? DEFAULT_MASKING_CONFIG;
1029
+ const toolRegistry = options?.toolRegistry;
1030
+ const iterStart = options?.currentIterationStartIndex;
1382
1031
  const toolResults = messages.map((msg, index) => ({ msg, index })).filter(({ msg }) => msg.role === "toolResult" || isCocoonToolResult(msg));
1383
- if (toolResults.length <= config.keepRecentCount) {
1032
+ const needsMasking = toolResults.length > config.keepRecentCount;
1033
+ const needsTruncation = iterStart !== void 0 && config.truncationThreshold > 0;
1034
+ if (!needsMasking && !needsTruncation) {
1384
1035
  return messages;
1385
1036
  }
1386
- const toMask = toolResults.slice(0, -config.keepRecentCount);
1387
1037
  const result = [...messages];
1388
- for (const { msg, index } of toMask) {
1389
- if (isCocoonToolResult(msg)) {
1038
+ if (needsMasking) {
1039
+ const toMask = toolResults.slice(0, -config.keepRecentCount);
1040
+ for (const { msg, index } of toMask) {
1041
+ if (isCocoonToolResult(msg)) {
1042
+ result[index] = {
1043
+ ...msg,
1044
+ content: [{ type: "text", text: "[Tool response masked]" }]
1045
+ };
1046
+ continue;
1047
+ }
1048
+ const toolMsg = msg;
1049
+ if (isExempt(toolMsg, config, toolRegistry)) continue;
1050
+ let summaryText = "";
1051
+ try {
1052
+ const textBlock = toolMsg.content.find((c2) => c2.type === "text");
1053
+ if (textBlock) {
1054
+ const parsed = JSON.parse(textBlock.text);
1055
+ if (parsed.data?.summary) {
1056
+ summaryText = ` - ${parsed.data.summary}`;
1057
+ } else if (parsed.data?.message) {
1058
+ summaryText = ` - ${parsed.data.message}`;
1059
+ }
1060
+ }
1061
+ } catch {
1062
+ }
1390
1063
  result[index] = {
1391
- ...msg,
1392
- content: [{ type: "text", text: "[Tool response masked]" }]
1064
+ ...toolMsg,
1065
+ content: [
1066
+ {
1067
+ type: "text",
1068
+ text: `[Tool: ${toolMsg.toolName} - ${toolMsg.isError ? "ERROR" : "OK"}${summaryText}]`
1069
+ }
1070
+ ]
1393
1071
  };
1394
- continue;
1395
- }
1396
- const toolMsg = msg;
1397
- if (config.keepErrorResults && toolMsg.isError) {
1398
- continue;
1399
1072
  }
1400
- if (toolRegistry) {
1401
- const category = toolRegistry.getToolCategory(toolMsg.toolName);
1402
- if (category === "data-bearing") {
1073
+ }
1074
+ if (needsTruncation) {
1075
+ const recentResults = needsMasking ? toolResults.slice(-config.keepRecentCount) : toolResults;
1076
+ for (const { msg, index } of recentResults) {
1077
+ if (index >= iterStart) continue;
1078
+ if (isCocoonToolResult(msg)) {
1079
+ const userMsg = msg;
1080
+ if (!Array.isArray(userMsg.content)) continue;
1081
+ const textBlock2 = userMsg.content.find((c2) => c2.type === "text");
1082
+ if (textBlock2 && textBlock2.text.length > config.truncationThreshold) {
1083
+ result[index] = {
1084
+ ...userMsg,
1085
+ content: [
1086
+ {
1087
+ type: "text",
1088
+ text: truncateToolResult(textBlock2.text, config.truncationKeepChars)
1089
+ }
1090
+ ]
1091
+ };
1092
+ }
1403
1093
  continue;
1404
1094
  }
1405
- }
1406
- let summaryText = "";
1407
- try {
1095
+ const toolMsg = msg;
1096
+ if (isExempt(toolMsg, config, toolRegistry)) continue;
1408
1097
  const textBlock = toolMsg.content.find((c2) => c2.type === "text");
1409
- if (textBlock) {
1410
- const parsed = JSON.parse(textBlock.text);
1411
- if (parsed.data?.summary) {
1412
- summaryText = ` - ${parsed.data.summary}`;
1413
- } else if (parsed.data?.message) {
1414
- summaryText = ` - ${parsed.data.message}`;
1415
- }
1416
- }
1417
- } catch {
1098
+ if (!textBlock || textBlock.text.length <= config.truncationThreshold) continue;
1099
+ result[index] = {
1100
+ ...toolMsg,
1101
+ content: [
1102
+ {
1103
+ type: "text",
1104
+ text: truncateToolResult(textBlock.text, config.truncationKeepChars)
1105
+ }
1106
+ ]
1107
+ };
1418
1108
  }
1419
- result[index] = {
1420
- ...toolMsg,
1421
- content: [
1422
- {
1423
- type: "text",
1424
- text: `[Tool: ${toolMsg.toolName} - ${toolMsg.isError ? "ERROR" : "OK"}${summaryText}]`
1425
- }
1426
- ]
1427
- };
1428
1109
  }
1429
1110
  return result;
1430
1111
  }
1431
1112
 
1432
1113
  // src/agent/runtime.ts
1433
- var log6 = createLogger("Agent");
1114
+ var log4 = createLogger("Agent");
1434
1115
  var globalTokenUsage = { totalTokens: 0, totalCost: 0 };
1435
1116
  function getTokenUsage() {
1436
1117
  return { ...globalTokenUsage };
@@ -1484,6 +1165,8 @@ var AgentRuntime = class {
1484
1165
  contextBuilder = null;
1485
1166
  toolRegistry = null;
1486
1167
  embedder = null;
1168
+ hookRunner;
1169
+ userHookEvaluator;
1487
1170
  constructor(config, soul, toolRegistry) {
1488
1171
  this.config = config;
1489
1172
  this.soul = soul ?? "";
@@ -1504,6 +1187,12 @@ var AgentRuntime = class {
1504
1187
  this.compactionManager = new CompactionManager(DEFAULT_COMPACTION_CONFIG);
1505
1188
  }
1506
1189
  }
1190
+ setHookRunner(runner) {
1191
+ this.hookRunner = runner;
1192
+ }
1193
+ setUserHookEvaluator(evaluator) {
1194
+ this.userHookEvaluator = evaluator;
1195
+ }
1507
1196
  initializeContextBuilder(embedder, vectorEnabled) {
1508
1197
  this.embedder = embedder;
1509
1198
  const db = getDatabase().getDb();
@@ -1528,15 +1217,62 @@ var AgentRuntime = class {
1528
1217
  messageId,
1529
1218
  replyContext
1530
1219
  } = opts;
1220
+ const effectiveIsGroup = isGroup ?? false;
1221
+ const processStartTime = Date.now();
1531
1222
  try {
1223
+ let userHookContext = "";
1224
+ if (this.userHookEvaluator) {
1225
+ const hookResult = this.userHookEvaluator.evaluate(userMessage);
1226
+ if (hookResult.blocked) {
1227
+ log4.info("Message blocked by keyword filter");
1228
+ return { content: hookResult.blockMessage ?? "", toolCalls: [] };
1229
+ }
1230
+ if (hookResult.additionalContext) {
1231
+ userHookContext = sanitizeForContext(hookResult.additionalContext);
1232
+ }
1233
+ }
1234
+ let effectiveMessage = userMessage;
1235
+ let hookMessageContext = "";
1236
+ if (this.hookRunner) {
1237
+ const msgEvent = {
1238
+ chatId,
1239
+ senderId: toolContext?.senderId ? String(toolContext.senderId) : chatId,
1240
+ senderName: userName ?? "",
1241
+ isGroup: effectiveIsGroup,
1242
+ isReply: !!replyContext,
1243
+ replyToMessageId: replyContext ? messageId : void 0,
1244
+ messageId: messageId ?? 0,
1245
+ timestamp: timestamp ?? Date.now(),
1246
+ text: userMessage,
1247
+ block: false,
1248
+ blockReason: "",
1249
+ additionalContext: ""
1250
+ };
1251
+ await this.hookRunner.runModifyingHook("message:receive", msgEvent);
1252
+ if (msgEvent.block) {
1253
+ log4.info(`\u{1F6AB} Message blocked by hook: ${msgEvent.blockReason || "no reason"}`);
1254
+ return { content: "", toolCalls: [] };
1255
+ }
1256
+ effectiveMessage = sanitizeForContext(msgEvent.text);
1257
+ if (msgEvent.additionalContext) {
1258
+ hookMessageContext = sanitizeForContext(msgEvent.additionalContext);
1259
+ }
1260
+ }
1532
1261
  let session = getOrCreateSession(chatId);
1533
1262
  const now = timestamp ?? Date.now();
1534
1263
  const resetPolicy = this.config.agent.session_reset_policy;
1535
1264
  if (shouldResetSession(session, resetPolicy)) {
1536
- log6.info(`\u{1F504} Auto-resetting session based on policy`);
1265
+ log4.info(`\u{1F504} Auto-resetting session based on policy`);
1266
+ if (this.hookRunner) {
1267
+ await this.hookRunner.runObservingHook("session:end", {
1268
+ sessionId: session.sessionId,
1269
+ chatId,
1270
+ messageCount: session.messageCount
1271
+ });
1272
+ }
1537
1273
  if (transcriptExists(session.sessionId)) {
1538
1274
  try {
1539
- log6.info(`\u{1F4BE} Saving memory before daily reset...`);
1275
+ log4.info(`\u{1F4BE} Saving memory before daily reset...`);
1540
1276
  const oldContext = loadContextFromTranscript(session.sessionId);
1541
1277
  await saveSessionMemory({
1542
1278
  oldSessionId: session.sessionId,
@@ -1547,18 +1283,26 @@ var AgentRuntime = class {
1547
1283
  provider: this.config.agent.provider,
1548
1284
  utilityModel: this.config.agent.utility_model
1549
1285
  });
1550
- log6.info(`\u2705 Memory saved before reset`);
1286
+ log4.info(`\u2705 Memory saved before reset`);
1551
1287
  } catch (error) {
1552
- log6.warn({ err: error }, `\u26A0\uFE0F Failed to save memory before reset`);
1288
+ log4.warn({ err: error }, `\u26A0\uFE0F Failed to save memory before reset`);
1553
1289
  }
1554
1290
  }
1555
1291
  session = resetSessionWithPolicy(chatId, resetPolicy);
1556
1292
  }
1557
1293
  let context = loadContextFromTranscript(session.sessionId);
1558
- if (context.messages.length > 0) {
1559
- log6.info(`\u{1F4D6} Loading existing session: ${session.sessionId}`);
1294
+ const isNewSession = context.messages.length === 0;
1295
+ if (!isNewSession) {
1296
+ log4.info(`\u{1F4D6} Loading existing session: ${session.sessionId}`);
1560
1297
  } else {
1561
- log6.info(`\u{1F195} Starting new session: ${session.sessionId}`);
1298
+ log4.info(`\u{1F195} Starting new session: ${session.sessionId}`);
1299
+ }
1300
+ if (this.hookRunner) {
1301
+ await this.hookRunner.runObservingHook("session:start", {
1302
+ sessionId: session.sessionId,
1303
+ chatId,
1304
+ isResume: !isNewSession
1305
+ });
1562
1306
  }
1563
1307
  const previousTimestamp = session.updatedAt;
1564
1308
  let formattedMessage = formatMessageEnvelope({
@@ -1569,8 +1313,8 @@ var AgentRuntime = class {
1569
1313
  senderRank,
1570
1314
  timestamp: now,
1571
1315
  previousTimestamp,
1572
- body: userMessage,
1573
- isGroup: isGroup ?? false,
1316
+ body: effectiveMessage,
1317
+ isGroup: effectiveIsGroup,
1574
1318
  hasMedia,
1575
1319
  mediaType,
1576
1320
  messageId,
@@ -1580,27 +1324,38 @@ var AgentRuntime = class {
1580
1324
  formattedMessage = `${pendingContext}
1581
1325
 
1582
1326
  ${formattedMessage}`;
1583
- log6.debug(`\u{1F4CB} Including ${pendingContext.split("\n").length - 1} pending messages`);
1327
+ log4.debug(`\u{1F4CB} Including ${pendingContext.split("\n").length - 1} pending messages`);
1584
1328
  }
1585
- log6.debug(`\u{1F4E8} Formatted message: ${formattedMessage.substring(0, 100)}...`);
1329
+ log4.debug(`\u{1F4E8} Formatted message: ${formattedMessage.substring(0, 100)}...`);
1586
1330
  const preview = formattedMessage.slice(0, 50).replace(/\n/g, " ");
1587
1331
  const who = senderUsername ? `@${senderUsername}` : userName;
1588
1332
  const msgType = isGroup ? `Group ${chatId} ${who}` : `DM ${who}`;
1589
- log6.info(`\u{1F4E8} ${msgType}: "${preview}${formattedMessage.length > 50 ? "..." : ""}"`);
1333
+ log4.info(`\u{1F4E8} ${msgType}: "${preview}${formattedMessage.length > 50 ? "..." : ""}"`);
1590
1334
  let relevantContext = "";
1591
1335
  let queryEmbedding;
1592
- const isNonTrivial = !isTrivialMessage(userMessage);
1336
+ const isNonTrivial = !isTrivialMessage(effectiveMessage);
1593
1337
  if (this.embedder && isNonTrivial) {
1594
1338
  try {
1595
- queryEmbedding = await this.embedder.embedQuery(userMessage);
1339
+ let searchQuery = effectiveMessage;
1340
+ const recentUserMsgs = context.messages.filter((m) => m.role === "user" && typeof m.content === "string").slice(-3).map((m) => {
1341
+ const text = m.content;
1342
+ const bodyMatch = text.match(/\] (.+)/s);
1343
+ return (bodyMatch ? bodyMatch[1] : text).trim();
1344
+ }).filter((t) => t.length > 0);
1345
+ if (recentUserMsgs.length > 0) {
1346
+ searchQuery = recentUserMsgs.join(" ") + " " + effectiveMessage;
1347
+ }
1348
+ queryEmbedding = await this.embedder.embedQuery(
1349
+ searchQuery.slice(0, EMBEDDING_QUERY_MAX_CHARS)
1350
+ );
1596
1351
  } catch (error) {
1597
- log6.warn({ err: error }, "Embedding computation failed");
1352
+ log4.warn({ err: error }, "Embedding computation failed");
1598
1353
  }
1599
1354
  }
1600
1355
  if (this.contextBuilder && isNonTrivial) {
1601
1356
  try {
1602
1357
  const dbContext = await this.contextBuilder.buildContext({
1603
- query: userMessage,
1358
+ query: effectiveMessage,
1604
1359
  chatId,
1605
1360
  includeAgentMemory: true,
1606
1361
  includeFeedHistory: true,
@@ -1628,12 +1383,12 @@ ${sanitizedFeed.join("\n")}`
1628
1383
  }
1629
1384
  if (contextParts.length > 0) {
1630
1385
  relevantContext = contextParts.join("\n\n");
1631
- log6.debug(
1386
+ log4.debug(
1632
1387
  `\u{1F50D} Found ${dbContext.relevantKnowledge.length} knowledge chunks, ${dbContext.relevantFeed.length} feed messages`
1633
1388
  );
1634
1389
  }
1635
1390
  } catch (error) {
1636
- log6.warn({ err: error }, "Context building failed");
1391
+ log4.warn({ err: error }, "Context building failed");
1637
1392
  }
1638
1393
  }
1639
1394
  const memoryStats = this.getMemoryStats();
@@ -1645,8 +1400,23 @@ ${statsContext}
1645
1400
  ${relevantContext}` : `You are in a Telegram conversation with chat ID: ${chatId}. Maintain conversation continuity.
1646
1401
 
1647
1402
  ${statsContext}`;
1403
+ let hookAdditionalContext = "";
1404
+ if (this.hookRunner) {
1405
+ const promptEvent = {
1406
+ chatId,
1407
+ sessionId: session.sessionId,
1408
+ isGroup: effectiveIsGroup,
1409
+ additionalContext: ""
1410
+ };
1411
+ await this.hookRunner.runModifyingHook("prompt:before", promptEvent);
1412
+ hookAdditionalContext = sanitizeForContext(promptEvent.additionalContext);
1413
+ }
1648
1414
  const compactionConfig = this.compactionManager.getConfig();
1649
1415
  const needsMemoryFlush = compactionConfig.enabled && compactionConfig.memoryFlushEnabled && context.messages.length > Math.floor((compactionConfig.maxMessages ?? 200) * 0.75);
1416
+ const allHookContext = [userHookContext, hookAdditionalContext, hookMessageContext].filter(Boolean).join("\n\n");
1417
+ const finalContext = additionalContext + (allHookContext ? `
1418
+
1419
+ ${allHookContext}` : "");
1650
1420
  const systemPrompt = buildSystemPrompt({
1651
1421
  soul: this.soul,
1652
1422
  userName,
@@ -1654,11 +1424,23 @@ ${statsContext}`;
1654
1424
  senderId: toolContext?.senderId,
1655
1425
  ownerName: this.config.telegram.owner_name,
1656
1426
  ownerUsername: this.config.telegram.owner_username,
1657
- context: additionalContext,
1658
- includeMemory: !isGroup,
1659
- includeStrategy: !isGroup,
1427
+ context: finalContext,
1428
+ includeMemory: !effectiveIsGroup,
1429
+ includeStrategy: !effectiveIsGroup,
1660
1430
  memoryFlushWarning: needsMemoryFlush
1661
1431
  });
1432
+ if (this.hookRunner) {
1433
+ const promptAfterEvent = {
1434
+ chatId,
1435
+ sessionId: session.sessionId,
1436
+ isGroup: effectiveIsGroup,
1437
+ promptLength: systemPrompt.length,
1438
+ sectionCount: (systemPrompt.match(/^#{1,3} /gm) || []).length,
1439
+ ragContextLength: relevantContext.length,
1440
+ hookContextLength: allHookContext.length
1441
+ };
1442
+ await this.hookRunner.runObservingHook("prompt:after", promptAfterEvent);
1443
+ }
1662
1444
  const userMsg = {
1663
1445
  role: "user",
1664
1446
  content: formattedMessage,
@@ -1674,7 +1456,7 @@ ${statsContext}`;
1674
1456
  this.config.agent.utility_model
1675
1457
  );
1676
1458
  if (preemptiveCompaction) {
1677
- log6.info(`\u{1F5DC}\uFE0F Preemptive compaction triggered, reloading session...`);
1459
+ log4.info(`\u{1F5DC}\uFE0F Preemptive compaction triggered, reloading session...`);
1678
1460
  session = getSession(chatId);
1679
1461
  context = loadContextFromTranscript(session.sessionId);
1680
1462
  context.messages.push(userMsg);
@@ -1686,20 +1468,20 @@ ${statsContext}`;
1686
1468
  let tools;
1687
1469
  {
1688
1470
  const toolIndex = this.toolRegistry?.getToolIndex();
1689
- const useRAG = toolIndex?.isIndexed && this.config.tool_rag?.enabled !== false && !isTrivialMessage(userMessage) && !(providerMeta.toolLimit === null && this.config.tool_rag?.skip_unlimited_providers !== false);
1471
+ const useRAG = toolIndex?.isIndexed && this.config.tool_rag?.enabled !== false && !isTrivialMessage(effectiveMessage) && !(providerMeta.toolLimit === null && this.config.tool_rag?.skip_unlimited_providers !== false);
1690
1472
  if (useRAG && this.toolRegistry && queryEmbedding) {
1691
1473
  tools = await this.toolRegistry.getForContextWithRAG(
1692
- userMessage,
1474
+ effectiveMessage,
1693
1475
  queryEmbedding,
1694
- isGroup ?? false,
1476
+ effectiveIsGroup,
1695
1477
  providerMeta.toolLimit,
1696
1478
  chatId,
1697
1479
  isAdmin
1698
1480
  );
1699
- log6.info(`\u{1F50D} Tool RAG: ${tools.length}/${this.toolRegistry.count} tools selected`);
1481
+ log4.info(`\u{1F50D} Tool RAG: ${tools.length}/${this.toolRegistry.count} tools selected`);
1700
1482
  } else {
1701
1483
  tools = this.toolRegistry?.getForContext(
1702
- isGroup ?? false,
1484
+ effectiveIsGroup,
1703
1485
  providerMeta.toolLimit,
1704
1486
  chatId,
1705
1487
  isAdmin
@@ -1715,14 +1497,15 @@ ${statsContext}`;
1715
1497
  const totalToolCalls = [];
1716
1498
  const accumulatedTexts = [];
1717
1499
  const accumulatedUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalCost: 0 };
1500
+ const seenToolSignatures = /* @__PURE__ */ new Set();
1718
1501
  while (iteration < maxIterations) {
1719
1502
  iteration++;
1720
- log6.debug(`\u{1F504} Agentic iteration ${iteration}/${maxIterations}`);
1721
- const maskedMessages = maskOldToolResults(
1722
- context.messages,
1723
- void 0,
1724
- this.toolRegistry ?? void 0
1725
- );
1503
+ log4.debug(`\u{1F504} Agentic iteration ${iteration}/${maxIterations}`);
1504
+ const iterationStartIndex = context.messages.length;
1505
+ const maskedMessages = maskOldToolResults(context.messages, {
1506
+ toolRegistry: this.toolRegistry ?? void 0,
1507
+ currentIterationStartIndex: iterationStartIndex
1508
+ });
1726
1509
  const maskedContext = { ...context, messages: maskedMessages };
1727
1510
  const response2 = await chatWithContext(this.config.agent, {
1728
1511
  systemPrompt,
@@ -1734,6 +1517,21 @@ ${statsContext}`;
1734
1517
  const assistantMsg = response2.message;
1735
1518
  if (assistantMsg.stopReason === "error") {
1736
1519
  const errorMsg = assistantMsg.errorMessage || "";
1520
+ if (this.hookRunner) {
1521
+ const errorCode = errorMsg.includes("429") || errorMsg.toLowerCase().includes("rate") ? "RATE_LIMIT" : isContextOverflowError(errorMsg) ? "CONTEXT_OVERFLOW" : errorMsg.includes("500") || errorMsg.includes("502") || errorMsg.includes("503") ? "PROVIDER_ERROR" : "UNKNOWN";
1522
+ const responseErrorEvent = {
1523
+ chatId,
1524
+ sessionId: session.sessionId,
1525
+ isGroup: effectiveIsGroup,
1526
+ error: errorMsg,
1527
+ errorCode,
1528
+ provider,
1529
+ model: this.config.agent.model,
1530
+ retryCount: rateLimitRetries + serverErrorRetries,
1531
+ durationMs: Date.now() - processStartTime
1532
+ };
1533
+ await this.hookRunner.runObservingHook("response:error", responseErrorEvent);
1534
+ }
1737
1535
  if (isContextOverflowError(errorMsg)) {
1738
1536
  overflowResets++;
1739
1537
  if (overflowResets > 1) {
@@ -1741,35 +1539,35 @@ ${statsContext}`;
1741
1539
  "Context overflow persists after session reset. Message may be too large for the model's context window."
1742
1540
  );
1743
1541
  }
1744
- log6.error(`\u{1F6A8} Context overflow detected: ${errorMsg}`);
1745
- log6.info(`\u{1F4BE} Saving session memory before reset...`);
1542
+ log4.error(`\u{1F6A8} Context overflow detected: ${errorMsg}`);
1543
+ log4.info(`\u{1F4BE} Saving session memory before reset...`);
1746
1544
  const summary = extractContextSummary(context, CONTEXT_OVERFLOW_SUMMARY_MESSAGES);
1747
1545
  appendToDailyLog(summary);
1748
- log6.info(`\u2705 Memory saved to daily log`);
1546
+ log4.info(`\u2705 Memory saved to daily log`);
1749
1547
  const archived = archiveTranscript(session.sessionId);
1750
1548
  if (!archived) {
1751
- log6.error(
1549
+ log4.error(
1752
1550
  `\u26A0\uFE0F Failed to archive transcript ${session.sessionId}, proceeding with reset anyway`
1753
1551
  );
1754
1552
  }
1755
- log6.info(`\u{1F504} Resetting session due to context overflow...`);
1553
+ log4.info(`\u{1F504} Resetting session due to context overflow...`);
1756
1554
  session = resetSession(chatId);
1757
1555
  context = { messages: [userMsg] };
1758
1556
  appendToTranscript(session.sessionId, userMsg);
1759
- log6.info(`\u{1F504} Retrying with fresh context...`);
1557
+ log4.info(`\u{1F504} Retrying with fresh context...`);
1760
1558
  continue;
1761
1559
  } else if (errorMsg.toLowerCase().includes("rate") || errorMsg.includes("429")) {
1762
1560
  rateLimitRetries++;
1763
1561
  if (rateLimitRetries <= RATE_LIMIT_MAX_RETRIES) {
1764
1562
  const delay = 1e3 * Math.pow(2, rateLimitRetries - 1);
1765
- log6.warn(
1563
+ log4.warn(
1766
1564
  `\u{1F6AB} Rate limited, retrying in ${delay}ms (attempt ${rateLimitRetries}/${RATE_LIMIT_MAX_RETRIES})...`
1767
1565
  );
1768
1566
  await new Promise((r3) => setTimeout(r3, delay));
1769
1567
  iteration--;
1770
1568
  continue;
1771
1569
  }
1772
- log6.error(`\u{1F6AB} Rate limited after ${RATE_LIMIT_MAX_RETRIES} retries: ${errorMsg}`);
1570
+ log4.error(`\u{1F6AB} Rate limited after ${RATE_LIMIT_MAX_RETRIES} retries: ${errorMsg}`);
1773
1571
  throw new Error(
1774
1572
  `API rate limited after ${RATE_LIMIT_MAX_RETRIES} retries. Please try again later.`
1775
1573
  );
@@ -1777,19 +1575,19 @@ ${statsContext}`;
1777
1575
  serverErrorRetries++;
1778
1576
  if (serverErrorRetries <= SERVER_ERROR_MAX_RETRIES) {
1779
1577
  const delay = 2e3 * Math.pow(2, serverErrorRetries - 1);
1780
- log6.warn(
1578
+ log4.warn(
1781
1579
  `\u{1F504} Server error, retrying in ${delay}ms (attempt ${serverErrorRetries}/${SERVER_ERROR_MAX_RETRIES})...`
1782
1580
  );
1783
1581
  await new Promise((r3) => setTimeout(r3, delay));
1784
1582
  iteration--;
1785
1583
  continue;
1786
1584
  }
1787
- log6.error(`\u{1F6A8} Server error after ${SERVER_ERROR_MAX_RETRIES} retries: ${errorMsg}`);
1585
+ log4.error(`\u{1F6A8} Server error after ${SERVER_ERROR_MAX_RETRIES} retries: ${errorMsg}`);
1788
1586
  throw new Error(
1789
1587
  `API server error after ${SERVER_ERROR_MAX_RETRIES} retries. The provider may be experiencing issues.`
1790
1588
  );
1791
1589
  } else {
1792
- log6.error(`\u{1F6A8} API error: ${errorMsg}`);
1590
+ log4.error(`\u{1F6A8} API error: ${errorMsg}`);
1793
1591
  throw new Error(`API error: ${errorMsg || "Unknown error"}`);
1794
1592
  }
1795
1593
  }
@@ -1806,55 +1604,156 @@ ${statsContext}`;
1806
1604
  }
1807
1605
  const toolCalls = response2.message.content.filter((block) => block.type === "toolCall");
1808
1606
  if (toolCalls.length === 0) {
1809
- log6.info(`\u{1F504} ${iteration}/${maxIterations} \u2192 done`);
1607
+ log4.info(`\u{1F504} ${iteration}/${maxIterations} \u2192 done`);
1810
1608
  finalResponse = response2;
1811
1609
  break;
1812
1610
  }
1813
1611
  if (!this.toolRegistry || !toolContext) {
1814
- log6.error("\u26A0\uFE0F Cannot execute tools: registry or context missing");
1612
+ log4.error("\u26A0\uFE0F Cannot execute tools: registry or context missing");
1815
1613
  break;
1816
1614
  }
1817
- log6.debug(`\u{1F527} Executing ${toolCalls.length} tool call(s)`);
1615
+ log4.debug(`\u{1F527} Executing ${toolCalls.length} tool call(s)`);
1818
1616
  context.messages.push(response2.message);
1819
1617
  const iterationToolNames = [];
1618
+ const fullContext = {
1619
+ ...toolContext,
1620
+ chatId,
1621
+ isGroup: effectiveIsGroup
1622
+ };
1623
+ const toolPlans = [];
1820
1624
  for (const block of toolCalls) {
1821
1625
  if (block.type !== "toolCall") continue;
1822
- const fullContext = {
1823
- ...toolContext,
1824
- chatId,
1825
- isGroup: isGroup ?? false
1626
+ let toolParams = block.arguments ?? {};
1627
+ let blocked = false;
1628
+ let blockReason = "";
1629
+ if (this.hookRunner) {
1630
+ const beforeEvent = {
1631
+ toolName: block.name,
1632
+ params: structuredClone(toolParams),
1633
+ chatId,
1634
+ isGroup: effectiveIsGroup,
1635
+ block: false,
1636
+ blockReason: ""
1637
+ };
1638
+ await this.hookRunner.runModifyingHook("tool:before", beforeEvent);
1639
+ if (beforeEvent.block) {
1640
+ blocked = true;
1641
+ blockReason = beforeEvent.blockReason || "Blocked by plugin hook";
1642
+ } else {
1643
+ toolParams = structuredClone(beforeEvent.params);
1644
+ }
1645
+ }
1646
+ toolPlans.push({ block, blocked, blockReason, params: toolParams });
1647
+ }
1648
+ const execResults = new Array(toolPlans.length);
1649
+ {
1650
+ let cursor = 0;
1651
+ const runWorker = async () => {
1652
+ while (cursor < toolPlans.length) {
1653
+ const idx = cursor++;
1654
+ const plan = toolPlans[idx];
1655
+ if (plan.blocked) {
1656
+ execResults[idx] = {
1657
+ result: { success: false, error: plan.blockReason },
1658
+ durationMs: 0
1659
+ };
1660
+ continue;
1661
+ }
1662
+ const startTime = Date.now();
1663
+ try {
1664
+ const result = await this.toolRegistry.execute(
1665
+ { ...plan.block, arguments: plan.params },
1666
+ fullContext
1667
+ );
1668
+ execResults[idx] = { result, durationMs: Date.now() - startTime };
1669
+ } catch (execErr) {
1670
+ const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
1671
+ const errStack = execErr instanceof Error ? execErr.stack : void 0;
1672
+ execResults[idx] = {
1673
+ result: { success: false, error: errMsg },
1674
+ durationMs: Date.now() - startTime,
1675
+ execError: { message: errMsg, stack: errStack }
1676
+ };
1677
+ }
1678
+ }
1826
1679
  };
1827
- const result = await this.toolRegistry.execute(block, fullContext);
1828
- log6.debug(`${block.name}: ${result.success ? "\u2713" : "\u2717"} ${result.error || ""}`);
1829
- iterationToolNames.push(`${block.name} ${result.success ? "\u2713" : "\u2717"}`);
1680
+ const workers = Math.min(TOOL_CONCURRENCY_LIMIT, toolPlans.length);
1681
+ await Promise.all(Array.from({ length: workers }, () => runWorker()));
1682
+ }
1683
+ for (let i = 0; i < toolPlans.length; i++) {
1684
+ const plan = toolPlans[i];
1685
+ const { block } = plan;
1686
+ const exec = execResults[i];
1687
+ if (exec.execError && this.hookRunner) {
1688
+ const errorEvent = {
1689
+ toolName: block.name,
1690
+ params: structuredClone(plan.params),
1691
+ error: exec.execError.message,
1692
+ stack: exec.execError.stack,
1693
+ chatId,
1694
+ isGroup: effectiveIsGroup,
1695
+ durationMs: exec.durationMs
1696
+ };
1697
+ await this.hookRunner.runObservingHook("tool:error", errorEvent);
1698
+ }
1699
+ if (this.hookRunner) {
1700
+ const afterEvent = {
1701
+ toolName: block.name,
1702
+ params: structuredClone(plan.params),
1703
+ result: {
1704
+ success: exec.result.success,
1705
+ data: exec.result.data,
1706
+ error: exec.result.error
1707
+ },
1708
+ durationMs: exec.durationMs,
1709
+ chatId,
1710
+ isGroup: effectiveIsGroup,
1711
+ ...plan.blocked ? { blocked: true, blockReason: plan.blockReason } : {}
1712
+ };
1713
+ await this.hookRunner.runObservingHook("tool:after", afterEvent);
1714
+ }
1715
+ log4.debug(`${block.name}: ${exec.result.success ? "\u2713" : "\u2717"} ${exec.result.error || ""}`);
1716
+ iterationToolNames.push(`${block.name} ${exec.result.success ? "\u2713" : "\u2717"}`);
1830
1717
  totalToolCalls.push({
1831
1718
  name: block.name,
1832
1719
  input: block.arguments
1833
1720
  });
1834
- let resultText = JSON.stringify(result, null, 2);
1721
+ let resultText = JSON.stringify(exec.result);
1835
1722
  if (resultText.length > MAX_TOOL_RESULT_SIZE) {
1836
- log6.warn(`\u26A0\uFE0F Tool result too large (${resultText.length} chars), truncating...`);
1837
- const data = result.data;
1723
+ log4.warn(`\u26A0\uFE0F Tool result too large (${resultText.length} chars), truncating...`);
1724
+ const data = exec.result.data;
1838
1725
  if (data?.summary || data?.message) {
1839
- resultText = JSON.stringify(
1840
- {
1841
- success: result.success,
1842
- data: {
1843
- summary: data.summary || data.message,
1844
- _truncated: true,
1845
- _originalSize: resultText.length,
1846
- _message: "Full data truncated. Use limit parameter for smaller results."
1847
- }
1848
- },
1849
- null,
1850
- 2
1851
- );
1726
+ resultText = JSON.stringify({
1727
+ success: exec.result.success,
1728
+ data: {
1729
+ summary: data.summary || data.message,
1730
+ _truncated: true,
1731
+ _originalSize: resultText.length,
1732
+ _message: "Full data truncated. Use limit parameter for smaller results."
1733
+ }
1734
+ });
1852
1735
  } else {
1853
- resultText = resultText.slice(0, MAX_TOOL_RESULT_SIZE) + "\n...[TRUNCATED]";
1736
+ const summarized = {
1737
+ _truncated: true,
1738
+ _originalSize: resultText.length,
1739
+ _message: "Full data truncated. Use limit parameter for smaller results."
1740
+ };
1741
+ if (data && typeof data === "object") {
1742
+ for (const [key, value] of Object.entries(data)) {
1743
+ if (Array.isArray(value)) {
1744
+ summarized[key] = `[${value.length} items]`;
1745
+ } else if (typeof value === "string" && value.length > 500) {
1746
+ summarized[key] = value.slice(0, 500) + "...[truncated]";
1747
+ } else {
1748
+ summarized[key] = value;
1749
+ }
1750
+ }
1751
+ }
1752
+ resultText = JSON.stringify({ success: exec.result.success, data: summarized });
1854
1753
  }
1855
1754
  }
1856
1755
  if (provider === "cocoon") {
1857
- const { wrapToolResult } = await import("./tool-adapter-IH5VGBOO.js");
1756
+ const { wrapToolResult } = await import("./tool-adapter-IVX2XQJE.js");
1858
1757
  const cocoonResultMsg = {
1859
1758
  role: "user",
1860
1759
  content: [
@@ -1878,21 +1777,33 @@ ${statsContext}`;
1878
1777
  text: resultText
1879
1778
  }
1880
1779
  ],
1881
- isError: !result.success,
1780
+ isError: !exec.result.success,
1882
1781
  timestamp: Date.now()
1883
1782
  };
1884
1783
  context.messages.push(toolResultMsg);
1885
1784
  appendToTranscript(session.sessionId, toolResultMsg);
1886
1785
  }
1887
1786
  }
1888
- log6.info(`\u{1F504} ${iteration}/${maxIterations} \u2192 ${iterationToolNames.join(", ")}`);
1787
+ log4.info(`\u{1F504} ${iteration}/${maxIterations} \u2192 ${iterationToolNames.join(", ")}`);
1788
+ const iterSignatures = toolPlans.map(
1789
+ (p2) => `${p2.block.name}:${JSON.stringify(p2.params, Object.keys(p2.params).sort())}`
1790
+ );
1791
+ const allDuplicates = iterSignatures.length > 0 && iterSignatures.every((sig) => seenToolSignatures.has(sig));
1792
+ for (const sig of iterSignatures) seenToolSignatures.add(sig);
1793
+ if (allDuplicates) {
1794
+ log4.warn(
1795
+ `\u{1F501} Loop stall detected: all ${iterSignatures.length} tool call(s) are repeats \u2014 breaking early`
1796
+ );
1797
+ finalResponse = response2;
1798
+ break;
1799
+ }
1889
1800
  if (iteration === maxIterations) {
1890
- log6.info(`\u26A0\uFE0F Max iterations reached (${maxIterations})`);
1801
+ log4.info(`\u26A0\uFE0F Max iterations reached (${maxIterations})`);
1891
1802
  finalResponse = response2;
1892
1803
  }
1893
1804
  }
1894
1805
  if (!finalResponse) {
1895
- log6.error("\u26A0\uFE0F Agentic loop exited early without final response");
1806
+ log4.error("\u26A0\uFE0F Agentic loop exited early without final response");
1896
1807
  return {
1897
1808
  content: "Internal error: Agent loop failed to produce a response.",
1898
1809
  toolCalls: []
@@ -1903,14 +1814,6 @@ ${statsContext}`;
1903
1814
  if (lastMsg?.role !== "assistant") {
1904
1815
  context.messages.push(response.message);
1905
1816
  }
1906
- const newSessionId = await this.compactionManager.checkAndCompact(
1907
- session.sessionId,
1908
- context,
1909
- getEffectiveApiKey(this.config.agent.provider, this.config.agent.api_key),
1910
- chatId,
1911
- this.config.agent.provider,
1912
- this.config.agent.utility_model
1913
- );
1914
1817
  const sessionUpdate = {
1915
1818
  updatedAt: Date.now(),
1916
1819
  messageCount: session.messageCount + 1,
@@ -1919,9 +1822,6 @@ ${statsContext}`;
1919
1822
  inputTokens: (session.inputTokens ?? 0) + accumulatedUsage.input + accumulatedUsage.cacheRead + accumulatedUsage.cacheWrite,
1920
1823
  outputTokens: (session.outputTokens ?? 0) + accumulatedUsage.output
1921
1824
  };
1922
- if (newSessionId) {
1923
- sessionUpdate.sessionId = newSessionId;
1924
- }
1925
1825
  updateSession(chatId, sessionUpdate);
1926
1826
  if (accumulatedUsage.input > 0 || accumulatedUsage.output > 0) {
1927
1827
  const u = accumulatedUsage;
@@ -1931,28 +1831,64 @@ ${statsContext}`;
1931
1831
  if (u.cacheRead) cacheParts.push(`${(u.cacheRead / 1e3).toFixed(1)}K cached`);
1932
1832
  if (u.cacheWrite) cacheParts.push(`${(u.cacheWrite / 1e3).toFixed(1)}K new`);
1933
1833
  const cacheInfo = cacheParts.length > 0 ? ` (${cacheParts.join(", ")})` : "";
1934
- log6.info(`\u{1F4B0} ${inK}K in${cacheInfo}, ${u.output} out | $${u.totalCost.toFixed(3)}`);
1834
+ log4.info(`\u{1F4B0} ${inK}K in${cacheInfo}, ${u.output} out | $${u.totalCost.toFixed(3)}`);
1935
1835
  globalTokenUsage.totalTokens += u.input + u.output + u.cacheRead + u.cacheWrite;
1936
1836
  globalTokenUsage.totalCost += u.totalCost;
1937
1837
  }
1938
1838
  let content = accumulatedTexts.join("\n").trim() || response.text;
1939
1839
  const usedTelegramSendTool = totalToolCalls.some((tc) => TELEGRAM_SEND_TOOLS.has(tc.name));
1940
1840
  if (!content && totalToolCalls.length > 0 && !usedTelegramSendTool) {
1941
- log6.warn("\u26A0\uFE0F Empty response after tool calls - generating fallback");
1841
+ log4.warn("\u26A0\uFE0F Empty response after tool calls - generating fallback");
1942
1842
  content = "I executed the requested action but couldn't generate a response. Please try again.";
1943
1843
  } else if (!content && usedTelegramSendTool) {
1944
- log6.info("\u2705 Response sent via Telegram tool - no additional text needed");
1844
+ log4.info("\u2705 Response sent via Telegram tool - no additional text needed");
1945
1845
  content = "";
1946
1846
  } else if (!content && accumulatedUsage.input === 0 && accumulatedUsage.output === 0) {
1947
- log6.warn("\u26A0\uFE0F Empty response with zero tokens - possible API issue");
1847
+ log4.warn("\u26A0\uFE0F Empty response with zero tokens - possible API issue");
1948
1848
  content = "I couldn't process your request. Please try again.";
1949
1849
  }
1850
+ let responseMetadata = {};
1851
+ if (this.hookRunner) {
1852
+ const responseBeforeEvent = {
1853
+ chatId,
1854
+ sessionId: session.sessionId,
1855
+ isGroup: effectiveIsGroup,
1856
+ originalText: content,
1857
+ text: content,
1858
+ block: false,
1859
+ blockReason: "",
1860
+ metadata: {}
1861
+ };
1862
+ await this.hookRunner.runModifyingHook("response:before", responseBeforeEvent);
1863
+ if (responseBeforeEvent.block) {
1864
+ log4.info(
1865
+ `\u{1F6AB} Response blocked by hook: ${responseBeforeEvent.blockReason || "no reason"}`
1866
+ );
1867
+ content = "";
1868
+ } else {
1869
+ content = responseBeforeEvent.text;
1870
+ }
1871
+ responseMetadata = responseBeforeEvent.metadata;
1872
+ }
1873
+ if (this.hookRunner) {
1874
+ const responseAfterEvent = {
1875
+ chatId,
1876
+ sessionId: session.sessionId,
1877
+ isGroup: effectiveIsGroup,
1878
+ text: content,
1879
+ durationMs: Date.now() - processStartTime,
1880
+ toolsUsed: totalToolCalls.map((tc) => tc.name),
1881
+ tokenUsage: accumulatedUsage.input > 0 || accumulatedUsage.output > 0 ? { input: accumulatedUsage.input, output: accumulatedUsage.output } : void 0,
1882
+ metadata: responseMetadata
1883
+ };
1884
+ await this.hookRunner.runObservingHook("response:after", responseAfterEvent);
1885
+ }
1950
1886
  return {
1951
1887
  content,
1952
1888
  toolCalls: totalToolCalls
1953
1889
  };
1954
1890
  } catch (error) {
1955
- log6.error({ err: error }, "Agent error");
1891
+ log4.error({ err: error }, "Agent error");
1956
1892
  throw error;
1957
1893
  }
1958
1894
  }
@@ -1965,7 +1901,7 @@ ${statsContext}`;
1965
1901
  ).run(chatId);
1966
1902
  db.prepare(`DELETE FROM tg_messages WHERE chat_id = ?`).run(chatId);
1967
1903
  resetSession(chatId);
1968
- log6.info(`\u{1F5D1}\uFE0F Cleared history for chat ${chatId}`);
1904
+ log4.info(`\u{1F5D1}\uFE0F Cleared history for chat ${chatId}`);
1969
1905
  }
1970
1906
  getConfig() {
1971
1907
  return this.config;
@@ -1986,7 +1922,7 @@ ${statsContext}`;
1986
1922
  }
1987
1923
  configureCompaction(config) {
1988
1924
  this.compactionManager.updateConfig(config);
1989
- log6.info({ config: this.compactionManager.getConfig() }, `\u{1F5DC}\uFE0F Compaction config updated`);
1925
+ log4.info({ config: this.compactionManager.getConfig() }, `\u{1F5DC}\uFE0F Compaction config updated`);
1990
1926
  }
1991
1927
  getCompactionConfig() {
1992
1928
  return this.compactionManager.getConfig();
@@ -2074,6 +2010,17 @@ function prefixButtons(rows, pluginName) {
2074
2010
 
2075
2011
  // src/bot/services/html-parser.ts
2076
2012
  import { Api as Api2 } from "telegram";
2013
+
2014
+ // src/utils/gramjs-bigint.ts
2015
+ import { randomBytes } from "crypto";
2016
+ function toLong(value) {
2017
+ return typeof value === "bigint" ? value : BigInt(value);
2018
+ }
2019
+ function randomLong() {
2020
+ return randomBytes(8).readBigUInt64BE();
2021
+ }
2022
+
2023
+ // src/bot/services/html-parser.ts
2077
2024
  function parseHtml(html) {
2078
2025
  const entities = [];
2079
2026
  let text = "";
@@ -2124,7 +2071,7 @@ function parseHtml(html) {
2124
2071
  new Api2.MessageEntityCustomEmoji({
2125
2072
  offset: open.offset,
2126
2073
  length,
2127
- documentId: BigInt(open.emojiId)
2074
+ documentId: toLong(open.emojiId)
2128
2075
  })
2129
2076
  );
2130
2077
  }
@@ -2185,7 +2132,7 @@ function unescapeHtml(text) {
2185
2132
  }
2186
2133
 
2187
2134
  // src/bot/inline-router.ts
2188
- var log7 = createLogger("InlineRouter");
2135
+ var log5 = createLogger("InlineRouter");
2189
2136
  var INLINE_TIMEOUT_MS = 5e3;
2190
2137
  var CALLBACK_TIMEOUT_MS = 15e3;
2191
2138
  function compileGlob(pattern) {
@@ -2206,11 +2153,11 @@ var InlineRouter = class {
2206
2153
  }
2207
2154
  registerPlugin(name, handlers) {
2208
2155
  this.plugins.set(name, handlers);
2209
- log7.info(`Registered plugin "${name}" for inline routing`);
2156
+ log5.info(`Registered plugin "${name}" for inline routing`);
2210
2157
  }
2211
2158
  unregisterPlugin(name) {
2212
2159
  this.plugins.delete(name);
2213
- log7.info(`Unregistered plugin "${name}" from inline routing`);
2160
+ log5.info(`Unregistered plugin "${name}" from inline routing`);
2214
2161
  }
2215
2162
  hasPlugin(name) {
2216
2163
  return this.plugins.has(name);
@@ -2262,11 +2209,14 @@ var InlineRouter = class {
2262
2209
  }
2263
2210
  async handleInlineQuery(ctx, pluginName, query, plugin) {
2264
2211
  try {
2212
+ const inlineQuery = ctx.inlineQuery;
2213
+ const from = ctx.from;
2214
+ if (!inlineQuery || !from || !plugin.onInlineQuery) return;
2265
2215
  const iqCtx = {
2266
2216
  query,
2267
- queryId: ctx.inlineQuery.id,
2268
- userId: ctx.from.id,
2269
- offset: ctx.inlineQuery.offset
2217
+ queryId: inlineQuery.id,
2218
+ userId: from.id,
2219
+ offset: inlineQuery.offset
2270
2220
  };
2271
2221
  const results = await withTimeout(
2272
2222
  plugin.onInlineQuery(iqCtx),
@@ -2279,7 +2229,7 @@ var InlineRouter = class {
2279
2229
  is_personal: true
2280
2230
  });
2281
2231
  } catch (error) {
2282
- log7.error({ err: error }, `Plugin "${pluginName}" inline query handler failed`);
2232
+ log5.error({ err: error }, `Plugin "${pluginName}" inline query handler failed`);
2283
2233
  try {
2284
2234
  await ctx.answerInlineQuery([], { cache_time: 0, is_personal: true });
2285
2235
  } catch {
@@ -2291,7 +2241,7 @@ var InlineRouter = class {
2291
2241
  try {
2292
2242
  let matchedHandler;
2293
2243
  let matchGroups = [];
2294
- for (const entry of plugin.onCallback) {
2244
+ for (const entry of plugin.onCallback ?? []) {
2295
2245
  const groups = globMatch(entry.regex, strippedData);
2296
2246
  if (groups !== null) {
2297
2247
  matchedHandler = entry.handler;
@@ -2304,14 +2254,17 @@ var InlineRouter = class {
2304
2254
  return;
2305
2255
  }
2306
2256
  const gramjsBotRef = this.gramjsBot;
2257
+ const callbackQuery = ctx.callbackQuery;
2258
+ const from = ctx.from;
2259
+ if (!from || !callbackQuery) return;
2307
2260
  const cbCtx = {
2308
2261
  data: strippedData,
2309
2262
  match: matchGroups,
2310
- userId: ctx.from.id,
2311
- username: ctx.from.username,
2312
- inlineMessageId: ctx.callbackQuery.inline_message_id,
2263
+ userId: from.id,
2264
+ username: from.username,
2265
+ inlineMessageId: callbackQuery.inline_message_id,
2313
2266
  chatId: ctx.chat?.id?.toString(),
2314
- messageId: ctx.callbackQuery.message?.message_id,
2267
+ messageId: callbackQuery.message?.message_id,
2315
2268
  async answer(text, alert) {
2316
2269
  if (!answered) {
2317
2270
  answered = true;
@@ -2320,7 +2273,7 @@ var InlineRouter = class {
2320
2273
  },
2321
2274
  async editMessage(text, opts) {
2322
2275
  const styledButtons = opts?.keyboard ? prefixButtons(opts.keyboard, pluginName) : void 0;
2323
- const inlineMsgId = ctx.callbackQuery.inline_message_id;
2276
+ const inlineMsgId = ctx.callbackQuery?.inline_message_id;
2324
2277
  if (inlineMsgId && gramjsBotRef?.isConnected() && styledButtons) {
2325
2278
  try {
2326
2279
  const strippedHtml = stripCustomEmoji(text);
@@ -2334,10 +2287,9 @@ var InlineRouter = class {
2334
2287
  });
2335
2288
  return;
2336
2289
  } catch (error) {
2337
- if (error?.errorMessage === "MESSAGE_NOT_MODIFIED") return;
2338
- log7.debug(
2339
- `GramJS edit failed, falling back to Grammy: ${error?.errorMessage || error}`
2340
- );
2290
+ const errMsg = error?.errorMessage;
2291
+ if (errMsg === "MESSAGE_NOT_MODIFIED") return;
2292
+ log5.debug(`GramJS edit failed, falling back to Grammy: ${errMsg || error}`);
2341
2293
  }
2342
2294
  }
2343
2295
  const replyMarkup = styledButtons ? toGrammyKeyboard(styledButtons) : void 0;
@@ -2357,7 +2309,7 @@ var InlineRouter = class {
2357
2309
  await ctx.answerCallbackQuery();
2358
2310
  }
2359
2311
  } catch (error) {
2360
- log7.error({ err: error }, `Plugin "${pluginName}" callback handler failed`);
2312
+ log5.error({ err: error }, `Plugin "${pluginName}" callback handler failed`);
2361
2313
  if (!answered) {
2362
2314
  try {
2363
2315
  await ctx.answerCallbackQuery({ text: "Error processing action" });
@@ -2368,17 +2320,19 @@ var InlineRouter = class {
2368
2320
  }
2369
2321
  async handleChosenResult(ctx, pluginName, plugin) {
2370
2322
  try {
2371
- const resultId = ctx.chosenInlineResult.result_id;
2323
+ const chosenResult = ctx.chosenInlineResult;
2324
+ if (!chosenResult || !plugin.onChosenResult) return;
2325
+ const resultId = chosenResult.result_id;
2372
2326
  const colonIdx = resultId.indexOf(":");
2373
2327
  const strippedResultId = colonIdx > 0 ? resultId.slice(colonIdx + 1) : resultId;
2374
2328
  const crCtx = {
2375
2329
  resultId: strippedResultId,
2376
- inlineMessageId: ctx.chosenInlineResult.inline_message_id,
2377
- query: ctx.chosenInlineResult.query
2330
+ inlineMessageId: chosenResult.inline_message_id,
2331
+ query: chosenResult.query
2378
2332
  };
2379
2333
  await plugin.onChosenResult(crCtx);
2380
2334
  } catch (error) {
2381
- log7.error({ err: error }, `Plugin "${pluginName}" chosen result handler failed`);
2335
+ log5.error({ err: error }, `Plugin "${pluginName}" chosen result handler failed`);
2382
2336
  }
2383
2337
  }
2384
2338
  /**
@@ -2457,14 +2411,38 @@ function withTimeout(promise, ms, message) {
2457
2411
 
2458
2412
  // src/agent/tools/plugin-loader.ts
2459
2413
  import { readdirSync as readdirSync2, readFileSync as readFileSync5, existsSync as existsSync6, statSync } from "fs";
2460
- import { join as join5 } from "path";
2414
+ import { join as join4 } from "path";
2461
2415
  import { pathToFileURL } from "url";
2462
2416
  import { execFile } from "child_process";
2417
+
2418
+ // src/agent/tools/plugin-config-store.ts
2419
+ function getPluginPriorities(db) {
2420
+ const rows = db.prepare("SELECT plugin_name, priority FROM plugin_config").all();
2421
+ const map = /* @__PURE__ */ new Map();
2422
+ for (const row of rows) {
2423
+ map.set(row.plugin_name, row.priority);
2424
+ }
2425
+ return map;
2426
+ }
2427
+ function setPluginPriority(db, pluginName, priority) {
2428
+ db.prepare(
2429
+ `INSERT INTO plugin_config (plugin_name, priority, updated_at)
2430
+ VALUES (?, ?, datetime('now'))
2431
+ ON CONFLICT(plugin_name) DO UPDATE SET
2432
+ priority = excluded.priority,
2433
+ updated_at = excluded.updated_at`
2434
+ ).run(pluginName, priority);
2435
+ }
2436
+ function resetPluginPriority(db, pluginName) {
2437
+ db.prepare("DELETE FROM plugin_config WHERE plugin_name = ?").run(pluginName);
2438
+ }
2439
+
2440
+ // src/agent/tools/plugin-loader.ts
2463
2441
  import { promisify } from "util";
2464
2442
 
2465
2443
  // src/agent/tools/plugin-validator.ts
2466
2444
  import { z } from "zod";
2467
- var log8 = createLogger("PluginValidator");
2445
+ var log6 = createLogger("PluginValidator");
2468
2446
  var ManifestSchema = z.object({
2469
2447
  name: z.string().min(1).max(64).regex(
2470
2448
  /^[a-z0-9][a-z0-9-]*$/,
@@ -2491,7 +2469,14 @@ var ManifestSchema = z.object({
2491
2469
  inlinePerMinute: z.number().positive().optional(),
2492
2470
  callbackPerMinute: z.number().positive().optional()
2493
2471
  }).optional()
2494
- }).optional()
2472
+ }).optional(),
2473
+ hooks: z.array(
2474
+ z.object({
2475
+ name: z.string().min(1).max(64),
2476
+ priority: z.number().optional(),
2477
+ description: z.string().max(256).optional()
2478
+ })
2479
+ ).optional()
2495
2480
  });
2496
2481
  function validateManifest(raw) {
2497
2482
  return ManifestSchema.parse(raw);
@@ -2500,20 +2485,20 @@ function validateToolDefs(defs, pluginName) {
2500
2485
  const valid = [];
2501
2486
  for (const def of defs) {
2502
2487
  if (!def || typeof def !== "object") {
2503
- log8.warn(`[${pluginName}] tool is not an object, skipping`);
2488
+ log6.warn(`[${pluginName}] tool is not an object, skipping`);
2504
2489
  continue;
2505
2490
  }
2506
2491
  const t = def;
2507
2492
  if (!t.name || typeof t.name !== "string") {
2508
- log8.warn(`[${pluginName}] tool missing 'name', skipping`);
2493
+ log6.warn(`[${pluginName}] tool missing 'name', skipping`);
2509
2494
  continue;
2510
2495
  }
2511
2496
  if (!t.description || typeof t.description !== "string") {
2512
- log8.warn(`[${pluginName}] tool "${t.name}" missing 'description', skipping`);
2497
+ log6.warn(`[${pluginName}] tool "${t.name}" missing 'description', skipping`);
2513
2498
  continue;
2514
2499
  }
2515
2500
  if (!t.execute || typeof t.execute !== "function") {
2516
- log8.warn(`[${pluginName}] tool "${t.name}" missing 'execute' function, skipping`);
2501
+ log6.warn(`[${pluginName}] tool "${t.name}" missing 'execute' function, skipping`);
2517
2502
  continue;
2518
2503
  }
2519
2504
  valid.push(t);
@@ -2550,8 +2535,19 @@ import { Address, SendMode } from "@ton/core";
2550
2535
 
2551
2536
  // src/ton/tx-lock.ts
2552
2537
  var pending = Promise.resolve();
2538
+ var TX_LOCK_TIMEOUT_MS = 6e4;
2553
2539
  function withTxLock(fn) {
2554
- const execute = pending.then(fn, fn);
2540
+ const guarded = () => {
2541
+ let timerId;
2542
+ const timeoutPromise = new Promise((_, reject) => {
2543
+ timerId = setTimeout(
2544
+ () => reject(new Error("TON tx-lock timeout (60s)")),
2545
+ TX_LOCK_TIMEOUT_MS
2546
+ );
2547
+ });
2548
+ return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timerId));
2549
+ };
2550
+ const execute = pending.then(guarded, guarded);
2555
2551
  pending = execute.then(
2556
2552
  () => {
2557
2553
  },
@@ -2562,25 +2558,25 @@ function withTxLock(fn) {
2562
2558
  }
2563
2559
 
2564
2560
  // src/ton/transfer.ts
2565
- var log9 = createLogger("TON");
2561
+ var log7 = createLogger("TON");
2566
2562
  async function sendTon(params) {
2567
2563
  return withTxLock(async () => {
2568
2564
  try {
2569
2565
  const { toAddress: toAddress2, amount, comment = "", bounce = false } = params;
2570
2566
  if (!Number.isFinite(amount) || amount <= 0) {
2571
- log9.error({ amount }, "Invalid transfer amount");
2567
+ log7.error({ amount }, "Invalid transfer amount");
2572
2568
  return null;
2573
2569
  }
2574
2570
  let recipientAddress;
2575
2571
  try {
2576
2572
  recipientAddress = Address.parse(toAddress2);
2577
2573
  } catch (e) {
2578
- log9.error({ err: e }, `Invalid recipient address: ${toAddress2}`);
2574
+ log7.error({ err: e }, `Invalid recipient address: ${toAddress2}`);
2579
2575
  return null;
2580
2576
  }
2581
2577
  const keyPair = await getKeyPair();
2582
2578
  if (!keyPair) {
2583
- log9.error("Wallet not initialized");
2579
+ log7.error("Wallet not initialized");
2584
2580
  return null;
2585
2581
  }
2586
2582
  const wallet = WalletContractV5R1.create({
@@ -2604,21 +2600,22 @@ async function sendTon(params) {
2604
2600
  ]
2605
2601
  });
2606
2602
  const pseudoHash = `${seqno}_${Date.now()}_${amount.toFixed(2)}`;
2607
- log9.info(`Sent ${amount} TON to ${toAddress2.slice(0, 8)}... - seqno: ${seqno}`);
2603
+ log7.info(`Sent ${amount} TON to ${toAddress2.slice(0, 8)}... - seqno: ${seqno}`);
2608
2604
  return pseudoHash;
2609
2605
  } catch (error) {
2610
- const status = error?.status || error?.response?.status;
2611
- if (status === 429 || status >= 500) {
2606
+ const err = error;
2607
+ const status = err?.status || err?.response?.status;
2608
+ if (status === 429 || status !== void 0 && status >= 500) {
2612
2609
  invalidateTonClientCache();
2613
2610
  }
2614
- log9.error({ err: error }, "Error sending TON");
2611
+ log7.error({ err: error }, "Error sending TON");
2615
2612
  throw error;
2616
2613
  }
2617
2614
  });
2618
2615
  }
2619
2616
 
2620
2617
  // src/utils/retry.ts
2621
- var log10 = createLogger("Utils");
2618
+ var log8 = createLogger("Utils");
2622
2619
  var DEFAULT_OPTIONS = {
2623
2620
  maxAttempts: RETRY_DEFAULT_MAX_ATTEMPTS,
2624
2621
  baseDelayMs: RETRY_DEFAULT_BASE_DELAY_MS,
@@ -2642,7 +2639,7 @@ async function withRetry(fn, options = {}) {
2642
2639
  return result;
2643
2640
  } catch (error) {
2644
2641
  lastError = error instanceof Error ? error : new Error(String(error));
2645
- log10.warn(`Retry attempt ${attempt}/${opts.maxAttempts} failed: ${lastError.message}`);
2642
+ log8.warn(`Retry attempt ${attempt}/${opts.maxAttempts} failed: ${lastError.message}`);
2646
2643
  if (attempt < opts.maxAttempts) {
2647
2644
  const delay = Math.min(opts.baseDelayMs * Math.pow(2, attempt - 1), opts.maxDelayMs);
2648
2645
  await sleep(delay);
@@ -3142,9 +3139,9 @@ var retryStatusCodes = /* @__PURE__ */ new Set([
3142
3139
  var nullBodyResponses = /* @__PURE__ */ new Set([101, 204, 205, 304]);
3143
3140
  function createFetch(globalOptions = {}) {
3144
3141
  const {
3145
- fetch: fetch2 = globalThis.fetch,
3142
+ fetch: fetch3 = globalThis.fetch,
3146
3143
  Headers: Headers2 = globalThis.Headers,
3147
- AbortController: AbortController2 = globalThis.AbortController
3144
+ AbortController: AbortController3 = globalThis.AbortController
3148
3145
  } = globalOptions;
3149
3146
  async function onError(context) {
3150
3147
  const isAbort = context.error && context.error.name === "AbortError" && !context.options.timeout || false;
@@ -3228,7 +3225,7 @@ function createFetch(globalOptions = {}) {
3228
3225
  }
3229
3226
  let abortTimeout;
3230
3227
  if (!context.options.signal && context.options.timeout) {
3231
- const controller = new AbortController2();
3228
+ const controller = new AbortController3();
3232
3229
  abortTimeout = setTimeout(() => {
3233
3230
  const error = new Error(
3234
3231
  "[TimeoutError]: The operation was aborted due to timeout"
@@ -3240,7 +3237,7 @@ function createFetch(globalOptions = {}) {
3240
3237
  context.options.signal = controller.signal;
3241
3238
  }
3242
3239
  try {
3243
- context.response = await fetch2(
3240
+ context.response = await fetch3(
3244
3241
  context.request,
3245
3242
  context.options
3246
3243
  );
@@ -3302,7 +3299,7 @@ function createFetch(globalOptions = {}) {
3302
3299
  return r3._data;
3303
3300
  };
3304
3301
  $fetch.raw = $fetchRaw;
3305
- $fetch.native = (...args) => fetch2(...args);
3302
+ $fetch.native = (...args) => fetch3(...args);
3306
3303
  $fetch.create = (defaultOptions = {}, customGlobalOptions = {}) => createFetch({
3307
3304
  ...globalOptions,
3308
3305
  ...customGlobalOptions,
@@ -3333,10 +3330,10 @@ function createNodeFetch() {
3333
3330
  return r(input, { ...nodeFetchOptions, ...init });
3334
3331
  };
3335
3332
  }
3336
- var fetch = globalThis.fetch ? (...args) => globalThis.fetch(...args) : createNodeFetch();
3333
+ var fetch2 = globalThis.fetch ? (...args) => globalThis.fetch(...args) : createNodeFetch();
3337
3334
  var Headers = globalThis.Headers || n;
3338
- var AbortController = globalThis.AbortController || T;
3339
- var ofetch = createFetch({ fetch, Headers, AbortController });
3335
+ var AbortController2 = globalThis.AbortController || T;
3336
+ var ofetch = createFetch({ fetch: fetch2, Headers, AbortController: AbortController2 });
3340
3337
 
3341
3338
  // node_modules/@ston-fi/api/dist/esm/index.js
3342
3339
  var __create = Object.create;
@@ -6120,7 +6117,7 @@ var DEDUST_GAS = {
6120
6117
  var NATIVE_TON_ADDRESS = "EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c";
6121
6118
 
6122
6119
  // src/agent/tools/dedust/asset-cache.ts
6123
- var log11 = createLogger("Tools");
6120
+ var log9 = createLogger("Tools");
6124
6121
  var ASSET_LIST_URL = "https://assets.dedust.io/list.json";
6125
6122
  var CACHE_TTL_MS = 10 * 60 * 1e3;
6126
6123
  var cachedAssets = [];
@@ -6139,7 +6136,7 @@ async function getAssetList() {
6139
6136
  return cachedAssets;
6140
6137
  } catch (error) {
6141
6138
  if (cachedAssets.length > 0) {
6142
- log11.warn({ err: error }, "Asset list fetch failed, using stale cache");
6139
+ log9.warn({ err: error }, "Asset list fetch failed, using stale cache");
6143
6140
  return cachedAssets;
6144
6141
  }
6145
6142
  throw error;
@@ -6265,7 +6262,7 @@ async function getDedustQuote(fromAsset, toAsset, amount, slippage, log13) {
6265
6262
  return null;
6266
6263
  }
6267
6264
  }
6268
- async function executeSTONfiSwap(params, log13) {
6265
+ async function executeSTONfiSwap(params, _log) {
6269
6266
  const { fromAsset, toAsset, amount, slippage = 0.01 } = params;
6270
6267
  const walletData = loadWallet();
6271
6268
  if (!walletData) {
@@ -6352,7 +6349,7 @@ async function executeSTONfiSwap(params, log13) {
6352
6349
  };
6353
6350
  });
6354
6351
  }
6355
- async function executeDedustSwap(params, log13) {
6352
+ async function executeDedustSwap(params, _log) {
6356
6353
  const { fromAsset, toAsset, amount, slippage = 0.01 } = params;
6357
6354
  const walletData = loadWallet();
6358
6355
  if (!walletData) {
@@ -6372,7 +6369,7 @@ async function executeDedustSwap(params, log13) {
6372
6369
  const fromDecimals = await getDecimals(isTonInput ? "ton" : fromAsset);
6373
6370
  const toDecimals = await getDecimals(isTonOutput ? "ton" : toAsset);
6374
6371
  const amountIn = toUnits(amount, fromDecimals);
6375
- const { amountOut, tradeFee } = await pool.getEstimatedSwapOut({
6372
+ const { amountOut } = await pool.getEstimatedSwapOut({
6376
6373
  assetIn: fromAssetObj,
6377
6374
  amountIn
6378
6375
  });
@@ -6648,7 +6645,10 @@ function createDnsSDK(log13) {
6648
6645
  throw new PluginSDKError(`No active auction found for ${normalized}`, "OPERATION_FAILED");
6649
6646
  }
6650
6647
  try {
6651
- await sendWalletMessage(Address6.parse(checkResult.nftAddress), toNano15(amount.toString()));
6648
+ await sendWalletMessage(
6649
+ Address6.parse(checkResult.nftAddress),
6650
+ toNano15(amount.toString())
6651
+ );
6652
6652
  return { domain: normalized, bidAmount: amount.toString(), success: true };
6653
6653
  } catch (err) {
6654
6654
  if (err instanceof PluginSDKError) throw err;
@@ -6745,7 +6745,7 @@ import {
6745
6745
  WalletContractV5R1 as WalletContractV5R14,
6746
6746
  internal as internal4
6747
6747
  } from "@ton/ton";
6748
- import { Address as TonAddress, beginCell as beginCell13, SendMode as SendMode4 } from "@ton/core";
6748
+ import { Address as TonAddress, beginCell as beginCell13, SendMode as SendMode4, storeMessage } from "@ton/core";
6749
6749
 
6750
6750
  // src/ton/format-transactions.ts
6751
6751
  import { fromNano } from "@ton/ton";
@@ -7114,26 +7114,222 @@ function createTonSDK(log13, db) {
7114
7114
  log13.error(`ton.getJettonInfo() TonAPI error: ${response.status}`);
7115
7115
  return null;
7116
7116
  }
7117
- const data = await response.json();
7118
- const metadata = data.metadata || {};
7119
- const decimals = parseInt(metadata.decimals || "9");
7117
+ const data = await response.json();
7118
+ const metadata = data.metadata || {};
7119
+ const decimals = parseInt(metadata.decimals || "9");
7120
+ return {
7121
+ address: metadata.address || jettonAddress,
7122
+ name: metadata.name || "Unknown",
7123
+ symbol: metadata.symbol || "UNKNOWN",
7124
+ decimals,
7125
+ totalSupply: data.total_supply || "0",
7126
+ holdersCount: data.holders_count || 0,
7127
+ verified: data.verification === "whitelist",
7128
+ description: metadata.description || void 0,
7129
+ image: data.preview || metadata.image || void 0
7130
+ };
7131
+ } catch (err) {
7132
+ log13.error("ton.getJettonInfo() failed:", err);
7133
+ return null;
7134
+ }
7135
+ },
7136
+ async sendJetton(jettonAddress, to, amount, opts) {
7137
+ const walletData = loadWallet();
7138
+ if (!walletData) {
7139
+ throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
7140
+ }
7141
+ if (!Number.isFinite(amount) || amount <= 0) {
7142
+ throw new PluginSDKError("Amount must be a positive number", "OPERATION_FAILED");
7143
+ }
7144
+ try {
7145
+ TonAddress.parse(to);
7146
+ } catch {
7147
+ throw new PluginSDKError("Invalid recipient address", "INVALID_ADDRESS");
7148
+ }
7149
+ try {
7150
+ const jettonsResponse = await tonapiFetch(
7151
+ `/accounts/${encodeURIComponent(walletData.address)}/jettons`
7152
+ );
7153
+ if (!jettonsResponse.ok) {
7154
+ throw new PluginSDKError(
7155
+ `Failed to fetch jetton balances: ${jettonsResponse.status}`,
7156
+ "OPERATION_FAILED"
7157
+ );
7158
+ }
7159
+ const jettonsData = await jettonsResponse.json();
7160
+ const jettonBalance = findJettonBalance(jettonsData.balances ?? [], jettonAddress);
7161
+ if (!jettonBalance) {
7162
+ throw new PluginSDKError(
7163
+ `You don't own any of this jetton: ${jettonAddress}`,
7164
+ "OPERATION_FAILED"
7165
+ );
7166
+ }
7167
+ const senderJettonWallet = jettonBalance.wallet_address.address;
7168
+ const decimals = jettonBalance.jetton.decimals ?? 9;
7169
+ const currentBalance = BigInt(jettonBalance.balance);
7170
+ const amountStr = amount.toFixed(decimals);
7171
+ const [whole, frac = ""] = amountStr.split(".");
7172
+ const amountInUnits = BigInt(whole + (frac + "0".repeat(decimals)).slice(0, decimals));
7173
+ if (amountInUnits > currentBalance) {
7174
+ const balStr = formatTokenBalance(currentBalance, decimals);
7175
+ throw new PluginSDKError(
7176
+ `Insufficient balance. Have ${balStr}, need ${amount}`,
7177
+ "OPERATION_FAILED"
7178
+ );
7179
+ }
7180
+ const comment = opts?.comment;
7181
+ let forwardPayload = beginCell13().endCell();
7182
+ if (comment) {
7183
+ forwardPayload = beginCell13().storeUint(0, 32).storeStringTail(comment).endCell();
7184
+ }
7185
+ const JETTON_TRANSFER_OP = 260734629;
7186
+ const messageBody = beginCell13().storeUint(JETTON_TRANSFER_OP, 32).storeUint(0, 64).storeCoins(amountInUnits).storeAddress(TonAddress.parse(to)).storeAddress(TonAddress.parse(walletData.address)).storeBit(false).storeCoins(comment ? tonToNano("0.01") : BigInt(1)).storeBit(comment ? 1 : 0).storeRef(comment ? forwardPayload : beginCell13().endCell()).endCell();
7187
+ const keyPair = await getKeyPair();
7188
+ if (!keyPair) {
7189
+ throw new PluginSDKError("Wallet key derivation failed", "OPERATION_FAILED");
7190
+ }
7191
+ const seqno = await withTxLock(async () => {
7192
+ const MAX_SEND_ATTEMPTS = 3;
7193
+ let lastErr;
7194
+ for (let attempt = 1; attempt <= MAX_SEND_ATTEMPTS; attempt++) {
7195
+ try {
7196
+ const wallet = WalletContractV5R14.create({
7197
+ workchain: 0,
7198
+ publicKey: keyPair.publicKey
7199
+ });
7200
+ const client = await getCachedTonClient();
7201
+ const walletContract = client.open(wallet);
7202
+ const seq = await walletContract.getSeqno();
7203
+ await walletContract.sendTransfer({
7204
+ seqno: seq,
7205
+ secretKey: keyPair.secretKey,
7206
+ sendMode: SendMode4.PAY_GAS_SEPARATELY,
7207
+ messages: [
7208
+ internal4({
7209
+ to: TonAddress.parse(senderJettonWallet),
7210
+ value: tonToNano("0.05"),
7211
+ body: messageBody,
7212
+ bounce: true
7213
+ })
7214
+ ]
7215
+ });
7216
+ return seq;
7217
+ } catch (err) {
7218
+ lastErr = err;
7219
+ const httpErr = isHttpError(err) ? err : void 0;
7220
+ const status = httpErr?.status || httpErr?.response?.status;
7221
+ const respData = httpErr?.response?.data;
7222
+ if (status === 429 || status && status >= 500) {
7223
+ invalidateTonClientCache();
7224
+ if (attempt < MAX_SEND_ATTEMPTS) {
7225
+ log13.warn(
7226
+ `sendJetton attempt ${attempt} failed (${status}): ${JSON.stringify(respData ?? err.message)}, retrying...`
7227
+ );
7228
+ await new Promise((r3) => setTimeout(r3, 1e3 * attempt));
7229
+ continue;
7230
+ }
7231
+ }
7232
+ throw err;
7233
+ }
7234
+ }
7235
+ throw lastErr;
7236
+ });
7237
+ return { success: true, seqno };
7238
+ } catch (err) {
7239
+ const outerHttpErr = isHttpError(err) ? err : void 0;
7240
+ const status = outerHttpErr?.status || outerHttpErr?.response?.status;
7241
+ if (status === 429 || status && status >= 500) {
7242
+ invalidateTonClientCache();
7243
+ }
7244
+ if (err instanceof PluginSDKError) throw err;
7245
+ throw new PluginSDKError(
7246
+ `Failed to send jetton: ${err instanceof Error ? err.message : String(err)}`,
7247
+ "OPERATION_FAILED"
7248
+ );
7249
+ }
7250
+ },
7251
+ async getJettonWalletAddress(ownerAddress, jettonAddress) {
7252
+ try {
7253
+ const response = await tonapiFetch(`/accounts/${encodeURIComponent(ownerAddress)}/jettons`);
7254
+ if (!response.ok) {
7255
+ log13.error(`ton.getJettonWalletAddress() TonAPI error: ${response.status}`);
7256
+ return null;
7257
+ }
7258
+ const data = await response.json();
7259
+ const match = findJettonBalance(data.balances ?? [], jettonAddress);
7260
+ return match ? match.wallet_address.address : null;
7261
+ } catch (err) {
7262
+ log13.error("ton.getJettonWalletAddress() failed:", err);
7263
+ return null;
7264
+ }
7265
+ },
7266
+ // ─── Signed Transfers (no broadcast) ──────────────────────────
7267
+ async createTransfer(to, amount, comment) {
7268
+ const walletData = loadWallet();
7269
+ if (!walletData) {
7270
+ throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
7271
+ }
7272
+ if (!Number.isFinite(amount) || amount <= 0) {
7273
+ throw new PluginSDKError("Amount must be a positive number", "OPERATION_FAILED");
7274
+ }
7275
+ try {
7276
+ TonAddress.parse(to);
7277
+ } catch {
7278
+ throw new PluginSDKError("Invalid TON address format", "INVALID_ADDRESS");
7279
+ }
7280
+ try {
7281
+ const keyPair = await getKeyPair();
7282
+ if (!keyPair) {
7283
+ throw new PluginSDKError("Wallet key derivation failed", "OPERATION_FAILED");
7284
+ }
7285
+ const boc = await withTxLock(async () => {
7286
+ const wallet = WalletContractV5R14.create({
7287
+ workchain: 0,
7288
+ publicKey: keyPair.publicKey
7289
+ });
7290
+ const client = await getCachedTonClient();
7291
+ const contract = client.open(wallet);
7292
+ const seqno = await contract.getSeqno();
7293
+ const transferCell = wallet.createTransfer({
7294
+ seqno,
7295
+ secretKey: keyPair.secretKey,
7296
+ sendMode: SendMode4.PAY_GAS_SEPARATELY,
7297
+ messages: [
7298
+ internal4({
7299
+ to: TonAddress.parse(to),
7300
+ value: tonToNano(amount),
7301
+ body: comment || "",
7302
+ bounce: false
7303
+ })
7304
+ ]
7305
+ });
7306
+ const extMsg = beginCell13().store(
7307
+ storeMessage({
7308
+ info: {
7309
+ type: "external-in",
7310
+ dest: wallet.address,
7311
+ importFee: 0n
7312
+ },
7313
+ init: seqno === 0 ? wallet.init : void 0,
7314
+ body: transferCell
7315
+ })
7316
+ ).endCell();
7317
+ return extMsg.toBoc().toString("base64");
7318
+ });
7120
7319
  return {
7121
- address: metadata.address || jettonAddress,
7122
- name: metadata.name || "Unknown",
7123
- symbol: metadata.symbol || "UNKNOWN",
7124
- decimals,
7125
- totalSupply: data.total_supply || "0",
7126
- holdersCount: data.holders_count || 0,
7127
- verified: data.verification === "whitelist",
7128
- description: metadata.description || void 0,
7129
- image: data.preview || metadata.image || void 0
7320
+ boc,
7321
+ publicKey: walletData.publicKey,
7322
+ walletVersion: "v5r1"
7130
7323
  };
7131
7324
  } catch (err) {
7132
- log13.error("ton.getJettonInfo() failed:", err);
7133
- return null;
7325
+ if (err instanceof PluginSDKError) throw err;
7326
+ throw new PluginSDKError(
7327
+ `Failed to create transfer: ${err instanceof Error ? err.message : String(err)}`,
7328
+ "OPERATION_FAILED"
7329
+ );
7134
7330
  }
7135
7331
  },
7136
- async sendJetton(jettonAddress, to, amount, opts) {
7332
+ async createJettonTransfer(jettonAddress, to, amount, opts) {
7137
7333
  const walletData = loadWallet();
7138
7334
  if (!walletData) {
7139
7335
  throw new PluginSDKError("Wallet not initialized", "WALLET_NOT_INITIALIZED");
@@ -7188,79 +7384,65 @@ function createTonSDK(log13, db) {
7188
7384
  if (!keyPair) {
7189
7385
  throw new PluginSDKError("Wallet key derivation failed", "OPERATION_FAILED");
7190
7386
  }
7191
- const seqno = await withTxLock(async () => {
7192
- const MAX_SEND_ATTEMPTS = 3;
7193
- let lastErr;
7194
- for (let attempt = 1; attempt <= MAX_SEND_ATTEMPTS; attempt++) {
7195
- try {
7196
- const wallet = WalletContractV5R14.create({
7197
- workchain: 0,
7198
- publicKey: keyPair.publicKey
7199
- });
7200
- const client = await getCachedTonClient();
7201
- const walletContract = client.open(wallet);
7202
- const seq = await walletContract.getSeqno();
7203
- await walletContract.sendTransfer({
7204
- seqno: seq,
7205
- secretKey: keyPair.secretKey,
7206
- sendMode: SendMode4.PAY_GAS_SEPARATELY,
7207
- messages: [
7208
- internal4({
7209
- to: TonAddress.parse(senderJettonWallet),
7210
- value: tonToNano("0.05"),
7211
- body: messageBody,
7212
- bounce: true
7213
- })
7214
- ]
7215
- });
7216
- return seq;
7217
- } catch (err) {
7218
- lastErr = err;
7219
- const status = err?.status || err?.response?.status;
7220
- const respData = err?.response?.data;
7221
- if (status === 429 || status && status >= 500) {
7222
- invalidateTonClientCache();
7223
- if (attempt < MAX_SEND_ATTEMPTS) {
7224
- log13.warn(
7225
- `sendJetton attempt ${attempt} failed (${status}): ${JSON.stringify(respData ?? err.message)}, retrying...`
7226
- );
7227
- await new Promise((r3) => setTimeout(r3, 1e3 * attempt));
7228
- continue;
7229
- }
7230
- }
7231
- throw err;
7232
- }
7233
- }
7234
- throw lastErr;
7387
+ const boc = await withTxLock(async () => {
7388
+ const wallet = WalletContractV5R14.create({
7389
+ workchain: 0,
7390
+ publicKey: keyPair.publicKey
7391
+ });
7392
+ const client = await getCachedTonClient();
7393
+ const walletContract = client.open(wallet);
7394
+ const seqno = await walletContract.getSeqno();
7395
+ const transferCell = wallet.createTransfer({
7396
+ seqno,
7397
+ secretKey: keyPair.secretKey,
7398
+ sendMode: SendMode4.PAY_GAS_SEPARATELY,
7399
+ messages: [
7400
+ internal4({
7401
+ to: TonAddress.parse(senderJettonWallet),
7402
+ value: tonToNano("0.05"),
7403
+ body: messageBody,
7404
+ bounce: true
7405
+ })
7406
+ ]
7407
+ });
7408
+ const extMsg = beginCell13().store(
7409
+ storeMessage({
7410
+ info: {
7411
+ type: "external-in",
7412
+ dest: wallet.address,
7413
+ importFee: 0n
7414
+ },
7415
+ init: seqno === 0 ? wallet.init : void 0,
7416
+ body: transferCell
7417
+ })
7418
+ ).endCell();
7419
+ return extMsg.toBoc().toString("base64");
7235
7420
  });
7236
- return { success: true, seqno };
7421
+ return {
7422
+ boc,
7423
+ publicKey: walletData.publicKey,
7424
+ walletVersion: "v5r1"
7425
+ };
7237
7426
  } catch (err) {
7238
- const status = err?.status || err?.response?.status;
7239
- if (status === 429 || status && status >= 500) {
7240
- invalidateTonClientCache();
7241
- }
7242
7427
  if (err instanceof PluginSDKError) throw err;
7243
7428
  throw new PluginSDKError(
7244
- `Failed to send jetton: ${err instanceof Error ? err.message : String(err)}`,
7429
+ `Failed to create jetton transfer: ${err instanceof Error ? err.message : String(err)}`,
7245
7430
  "OPERATION_FAILED"
7246
7431
  );
7247
7432
  }
7248
7433
  },
7249
- async getJettonWalletAddress(ownerAddress, jettonAddress) {
7434
+ getPublicKey() {
7250
7435
  try {
7251
- const response = await tonapiFetch(`/accounts/${encodeURIComponent(ownerAddress)}/jettons`);
7252
- if (!response.ok) {
7253
- log13.error(`ton.getJettonWalletAddress() TonAPI error: ${response.status}`);
7254
- return null;
7255
- }
7256
- const data = await response.json();
7257
- const match = findJettonBalance(data.balances ?? [], jettonAddress);
7258
- return match ? match.wallet_address.address : null;
7436
+ const wallet = loadWallet();
7437
+ return wallet?.publicKey ?? null;
7259
7438
  } catch (err) {
7260
- log13.error("ton.getJettonWalletAddress() failed:", err);
7439
+ log13.error("ton.getPublicKey() failed:", err);
7261
7440
  return null;
7262
7441
  }
7263
7442
  },
7443
+ getWalletVersion() {
7444
+ return "v5r1";
7445
+ },
7264
7446
  // ─── NFT ─────────────────────────────────────────────────────
7265
7447
  async getNftItems(ownerAddress) {
7266
7448
  try {
@@ -7475,15 +7657,6 @@ function mapNftItem(item) {
7475
7657
  // src/sdk/telegram.ts
7476
7658
  import { Api as Api3 } from "telegram";
7477
7659
 
7478
- // src/utils/gramjs-bigint.ts
7479
- import { randomBytes } from "crypto";
7480
- function toLong(value) {
7481
- return typeof value === "bigint" ? value : BigInt(value);
7482
- }
7483
- function randomLong() {
7484
- return randomBytes(8).readBigUInt64BE();
7485
- }
7486
-
7487
7660
  // src/sdk/telegram-utils.ts
7488
7661
  function requireBridge(bridge) {
7489
7662
  if (!bridge.isAvailable()) {
@@ -7657,7 +7830,7 @@ function createTelegramMessagesSDK(bridge, log13) {
7657
7830
  limit,
7658
7831
  maxId: 0,
7659
7832
  minId: 0,
7660
- hash: 0n
7833
+ hash: toLong(0n)
7661
7834
  })
7662
7835
  );
7663
7836
  const messages = [];
@@ -7820,10 +7993,12 @@ function createTelegramMessagesSDK(bridge, log13) {
7820
7993
  if (!messages || messages.length === 0 || !messages[0].media) {
7821
7994
  return null;
7822
7995
  }
7823
- const doc = messages[0].media?.document;
7824
- if (doc?.size && Number(doc.size) > MAX_DOWNLOAD_SIZE) {
7996
+ const media = messages[0].media;
7997
+ const doc = media && "document" in media ? media.document : void 0;
7998
+ const docSize = doc && "size" in doc ? doc.size : void 0;
7999
+ if (docSize && Number(docSize) > MAX_DOWNLOAD_SIZE) {
7825
8000
  throw new PluginSDKError(
7826
- `File too large (${Math.round(Number(doc.size) / 1024 / 1024)}MB). Max: 50MB`,
8001
+ `File too large (${Math.round(Number(docSize) / 1024 / 1024)}MB). Max: 50MB`,
7827
8002
  "OPERATION_FAILED"
7828
8003
  );
7829
8004
  }
@@ -7847,7 +8022,7 @@ function createTelegramMessagesSDK(bridge, log13) {
7847
8022
  const result = await gramJsClient.invoke(
7848
8023
  new Api4.messages.GetScheduledHistory({
7849
8024
  peer,
7850
- hash: 0n
8025
+ hash: toLong(0n)
7851
8026
  })
7852
8027
  );
7853
8028
  const messages = [];
@@ -8005,7 +8180,6 @@ function createTelegramSocialSDK(bridge, log13) {
8005
8180
  requireBridge2();
8006
8181
  try {
8007
8182
  const client = getClient2();
8008
- const Api4 = await getApi();
8009
8183
  let entity;
8010
8184
  try {
8011
8185
  const id = typeof userId === "string" ? userId.replace("@", "") : userId.toString();
@@ -8329,7 +8503,7 @@ function createTelegramSocialSDK(bridge, log13) {
8329
8503
  const user = await client.getInputEntity(userId.toString());
8330
8504
  const invoiceData = {
8331
8505
  peer: user,
8332
- giftId: BigInt(giftId),
8506
+ giftId: toLong(giftId),
8333
8507
  hideName: opts?.anonymous ?? false,
8334
8508
  message: opts?.message ? new Api4.TextWithEntities({ text: opts.message, entities: [] }) : void 0
8335
8509
  };
@@ -8413,7 +8587,7 @@ function createTelegramSocialSDK(bridge, log13) {
8413
8587
  const Api4 = await getApi();
8414
8588
  const result = await client.invoke(
8415
8589
  new Api4.payments.GetResaleStarGifts({
8416
- giftId: BigInt(giftId),
8590
+ giftId: toLong(giftId),
8417
8591
  offset: "",
8418
8592
  limit: limit ?? 50
8419
8593
  })
@@ -8572,7 +8746,7 @@ function createTelegramSocialSDK(bridge, log13) {
8572
8746
  new Api4.payments.UpdateStarGiftPrice({
8573
8747
  stargift: new Api4.InputSavedStarGiftUser({ msgId }),
8574
8748
  resellAmount: new Api4.StarsAmount({
8575
- amount: BigInt(price),
8749
+ amount: toLong(price),
8576
8750
  nanos: 0
8577
8751
  })
8578
8752
  })
@@ -8703,7 +8877,7 @@ function createTelegramSocialSDK(bridge, log13) {
8703
8877
  new Api4.payments.SendStarGiftOffer({
8704
8878
  peer,
8705
8879
  slug: giftSlug,
8706
- price: new Api4.StarsAmount({ amount: BigInt(price), nanos: 0 }),
8880
+ price: new Api4.StarsAmount({ amount: toLong(price), nanos: 0 }),
8707
8881
  duration,
8708
8882
  randomId: randomLong()
8709
8883
  })
@@ -8723,7 +8897,7 @@ function createTelegramSocialSDK(bridge, log13) {
8723
8897
  const client = getClient2();
8724
8898
  const { Api: Api4, helpers } = await import("telegram");
8725
8899
  const { CustomFile } = await import("telegram/client/uploads.js");
8726
- const { readFileSync: readFileSync7, statSync: statSync2 } = await import("fs");
8900
+ const { readFileSync: readFileSync8, statSync: statSync2 } = await import("fs");
8727
8901
  const { basename: basename2 } = await import("path");
8728
8902
  const { resolve: resolve2, normalize: normalize2 } = await import("path");
8729
8903
  const { homedir: homedir3 } = await import("os");
@@ -8748,7 +8922,7 @@ function createTelegramSocialSDK(bridge, log13) {
8748
8922
  }
8749
8923
  const fileName = basename2(filePath);
8750
8924
  const fileSize = statSync2(filePath).size;
8751
- const fileBuffer = readFileSync7(filePath);
8925
+ const fileBuffer = readFileSync8(filePath);
8752
8926
  const isVideo = filePath.toLowerCase().match(/\.(mp4|mov|avi|webm|mkv|m4v)$/);
8753
8927
  const customFile = new CustomFile(fileName, fileSize, filePath, fileBuffer);
8754
8928
  const uploadedFile = await client.uploadFile({
@@ -8941,10 +9115,10 @@ function createTelegramSDK(bridge, log13) {
8941
9115
 
8942
9116
  // src/sdk/secrets.ts
8943
9117
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync5 } from "fs";
8944
- import { join as join4 } from "path";
8945
- var SECRETS_DIR = join4(TELETON_ROOT, "plugins", "data");
9118
+ import { join as join3 } from "path";
9119
+ var SECRETS_DIR = join3(TELETON_ROOT, "plugins", "data");
8946
9120
  function getSecretsPath(pluginName) {
8947
- return join4(SECRETS_DIR, `${pluginName}.secrets.json`);
9121
+ return join3(SECRETS_DIR, `${pluginName}.secrets.json`);
8948
9122
  }
8949
9123
  function readSecretsFile(pluginName) {
8950
9124
  const filePath = getSecretsPath(pluginName);
@@ -9102,7 +9276,7 @@ function createBotSDK(router, gramjsBot, grammyBot, pluginName, manifest, rateLi
9102
9276
  const callbackLimit = manifest.rateLimits?.callbackPerMinute ?? 60;
9103
9277
  const handlers = {};
9104
9278
  function syncToRouter() {
9105
- router.registerPlugin(pluginName, { ...handlers });
9279
+ router?.registerPlugin(pluginName, { ...handlers });
9106
9280
  }
9107
9281
  const sdk = {
9108
9282
  get isAvailable() {
@@ -9269,6 +9443,36 @@ function createPluginSDK(deps, opts) {
9269
9443
  );
9270
9444
  if (result) cachedBot = result;
9271
9445
  return result;
9446
+ },
9447
+ on(hookName, handler, onOpts) {
9448
+ if (!opts.hookRegistry) {
9449
+ log13.warn(`Hook registration unavailable \u2014 sdk.on() ignored`);
9450
+ return;
9451
+ }
9452
+ if (opts.declaredHooks) {
9453
+ const declared = opts.declaredHooks.some((h2) => h2.name === hookName);
9454
+ if (!declared) {
9455
+ log13.warn(`Hook "${hookName}" not declared in manifest \u2014 registration rejected`);
9456
+ return;
9457
+ }
9458
+ }
9459
+ const rawPriority = Number(onOpts?.priority) || 0;
9460
+ const clampedPriority = Math.max(-1e3, Math.min(1e3, rawPriority));
9461
+ if (rawPriority !== clampedPriority) {
9462
+ log13.debug(`Hook "${hookName}" priority ${rawPriority} clamped to ${clampedPriority}`);
9463
+ }
9464
+ const registered = opts.hookRegistry.register({
9465
+ pluginId: opts.pluginName,
9466
+ hookName,
9467
+ handler,
9468
+ priority: clampedPriority,
9469
+ globalPriority: opts.globalPriority ?? 0
9470
+ });
9471
+ if (!registered) {
9472
+ log13.warn(
9473
+ `Hook registration limit reached for plugin "${opts.pluginName}" \u2014 "${hookName}" rejected`
9474
+ );
9475
+ }
9272
9476
  }
9273
9477
  };
9274
9478
  return Object.freeze(sdk);
@@ -9329,23 +9533,76 @@ function semverSatisfies(current, range) {
9329
9533
  return cur.major === req.major && cur.minor === req.minor && cur.patch === req.patch;
9330
9534
  }
9331
9535
 
9536
+ // src/sdk/hooks/registry.ts
9537
+ var MAX_HOOKS_PER_PLUGIN = 100;
9538
+ var HookRegistry = class {
9539
+ hooks = [];
9540
+ hookMap = /* @__PURE__ */ new Map();
9541
+ rebuildMap() {
9542
+ this.hookMap.clear();
9543
+ for (const h2 of this.hooks) {
9544
+ let arr = this.hookMap.get(h2.hookName);
9545
+ if (!arr) {
9546
+ arr = [];
9547
+ this.hookMap.set(h2.hookName, arr);
9548
+ }
9549
+ arr.push(h2);
9550
+ }
9551
+ for (const arr of this.hookMap.values()) {
9552
+ arr.sort((a, b) => {
9553
+ const aPrio = a.globalPriority + a.priority;
9554
+ const bPrio = b.globalPriority + b.priority;
9555
+ return aPrio - bPrio;
9556
+ });
9557
+ }
9558
+ }
9559
+ register(reg) {
9560
+ const pluginHookCount = this.hooks.filter((h2) => h2.pluginId === reg.pluginId).length;
9561
+ if (pluginHookCount >= MAX_HOOKS_PER_PLUGIN) {
9562
+ return false;
9563
+ }
9564
+ this.hooks.push({ ...reg, globalPriority: reg.globalPriority ?? 0 });
9565
+ this.rebuildMap();
9566
+ return true;
9567
+ }
9568
+ getHooks(name) {
9569
+ return this.hookMap.get(name) ?? [];
9570
+ }
9571
+ hasHooks(name) {
9572
+ return (this.hookMap.get(name)?.length ?? 0) > 0;
9573
+ }
9574
+ hasAnyHooks() {
9575
+ return this.hooks.length > 0;
9576
+ }
9577
+ unregister(pluginId) {
9578
+ const before = this.hooks.length;
9579
+ this.hooks = this.hooks.filter((h2) => h2.pluginId !== pluginId);
9580
+ this.rebuildMap();
9581
+ return before - this.hooks.length;
9582
+ }
9583
+ clear() {
9584
+ this.hooks = [];
9585
+ this.hookMap.clear();
9586
+ }
9587
+ };
9588
+
9332
9589
  // src/agent/tools/plugin-loader.ts
9333
9590
  var execFileAsync = promisify(execFile);
9334
- var log12 = createLogger("PluginLoader");
9335
- var PLUGIN_DATA_DIR = join5(TELETON_ROOT, "plugins", "data");
9336
- function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
9591
+ var log10 = createLogger("PluginLoader");
9592
+ var PLUGIN_DATA_DIR = join4(TELETON_ROOT, "plugins", "data");
9593
+ function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps, hookRegistry, pluginPriorities) {
9337
9594
  let manifest = null;
9338
9595
  if (raw.manifest) {
9339
9596
  try {
9340
9597
  manifest = validateManifest(raw.manifest);
9341
9598
  } catch (err) {
9342
- log12.warn(
9599
+ log10.warn(
9343
9600
  `[${entryName}] invalid manifest, ignoring: ${err instanceof Error ? err.message : err}`
9344
9601
  );
9345
9602
  }
9346
9603
  }
9347
9604
  if (!manifest) {
9348
- const manifestPath = join5(WORKSPACE_PATHS.PLUGINS_DIR, entryName, "manifest.json");
9605
+ const manifestPath = join4(WORKSPACE_PATHS.PLUGINS_DIR, entryName, "manifest.json");
9349
9606
  try {
9350
9607
  if (existsSync6(manifestPath)) {
9351
9608
  const diskManifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
@@ -9363,6 +9620,7 @@ function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
9363
9620
  }
9364
9621
  const pluginName = manifest?.name ?? entryName.replace(/\.js$/, "");
9365
9622
  const pluginVersion = manifest?.version ?? "0.0.0";
9623
+ const globalPriority = pluginPriorities?.get(pluginName) ?? 0;
9366
9624
  if (manifest?.dependencies) {
9367
9625
  for (const dep of manifest.dependencies) {
9368
9626
  if (!loadedModuleNames.includes(dep)) {
@@ -9422,10 +9680,10 @@ function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
9422
9680
  },
9423
9681
  migrate() {
9424
9682
  try {
9425
- const dbPath = join5(PLUGIN_DATA_DIR, `${pluginName}.db`);
9683
+ const dbPath = join4(PLUGIN_DATA_DIR, `${pluginName}.db`);
9426
9684
  pluginDb = openModuleDb(dbPath);
9427
9685
  if (hasMigrate) {
9428
- raw.migrate(pluginDb);
9686
+ raw.migrate?.(pluginDb);
9429
9687
  const pluginTables = pluginDb.prepare(
9430
9688
  `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`
9431
9689
  ).all().map((t) => t.name).filter((n2) => n2 !== "_kv");
@@ -9453,7 +9711,10 @@ function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
9453
9711
  db: pluginDb,
9454
9712
  sanitizedConfig,
9455
9713
  pluginConfig,
9456
- botManifest: manifest?.bot
9714
+ botManifest: manifest?.bot,
9715
+ hookRegistry,
9716
+ declaredHooks: manifest?.hooks,
9717
+ globalPriority
9457
9718
  });
9458
9719
  toolDefs = raw.tools(sdk);
9459
9720
  } else if (Array.isArray(raw.tools)) {
@@ -9524,51 +9785,59 @@ function adaptPlugin(raw, entryName, config, loadedModuleNames, sdkDeps) {
9524
9785
  return module;
9525
9786
  }
9526
9787
  async function ensurePluginDeps(pluginDir, pluginEntry) {
9527
- const pkgJson = join5(pluginDir, "package.json");
9528
- const lockfile = join5(pluginDir, "package-lock.json");
9529
- const nodeModules = join5(pluginDir, "node_modules");
9788
+ const pkgJson = join4(pluginDir, "package.json");
9789
+ const lockfile = join4(pluginDir, "package-lock.json");
9790
+ const nodeModules = join4(pluginDir, "node_modules");
9530
9791
  if (!existsSync6(pkgJson)) return;
9531
9792
  if (!existsSync6(lockfile)) {
9532
- log12.warn(
9793
+ log10.warn(
9533
9794
  `[${pluginEntry}] package.json without package-lock.json \u2014 skipping (lockfile required)`
9534
9795
  );
9535
9796
  return;
9536
9797
  }
9537
9798
  if (existsSync6(nodeModules)) {
9538
- const marker = join5(nodeModules, ".package-lock.json");
9799
+ const marker = join4(nodeModules, ".package-lock.json");
9539
9800
  if (existsSync6(marker) && statSync(marker).mtimeMs >= statSync(lockfile).mtimeMs) return;
9540
9801
  }
9541
- log12.info(`[${pluginEntry}] Installing dependencies...`);
9802
+ log10.info(`[${pluginEntry}] Installing dependencies...`);
9542
9803
  try {
9543
9804
  await execFileAsync("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], {
9544
9805
  cwd: pluginDir,
9545
9806
  timeout: 6e4,
9546
9807
  env: { ...process.env, NODE_ENV: "production" }
9547
9808
  });
9548
- log12.info(`[${pluginEntry}] Dependencies installed`);
9809
+ log10.info(`[${pluginEntry}] Dependencies installed`);
9549
9810
  } catch (err) {
9550
- log12.error(`[${pluginEntry}] Failed to install deps: ${String(err).slice(0, 300)}`);
9811
+ log10.error(`[${pluginEntry}] Failed to install deps: ${String(err).slice(0, 300)}`);
9551
9812
  }
9552
9813
  }
9553
- async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps) {
9814
+ async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps, db) {
9815
+ const hookRegistry = new HookRegistry();
9554
9816
  const pluginsDir = WORKSPACE_PATHS.PLUGINS_DIR;
9555
9817
  if (!existsSync6(pluginsDir)) {
9556
- return [];
9818
+ return { modules: [], hookRegistry };
9819
+ }
9820
+ let pluginPriorities = /* @__PURE__ */ new Map();
9821
+ if (db) {
9822
+ try {
9823
+ pluginPriorities = getPluginPriorities(db);
9824
+ } catch {
9825
+ }
9557
9826
  }
9558
- const entries = readdirSync2(pluginsDir);
9827
+ const entries = readdirSync2(pluginsDir).sort();
9559
9828
  const modules = [];
9560
9829
  const loadedNames = /* @__PURE__ */ new Set();
9561
9830
  const pluginPaths = [];
9562
9831
  for (const entry of entries) {
9563
9832
  if (entry === "data") continue;
9564
- const entryPath = join5(pluginsDir, entry);
9833
+ const entryPath = join4(pluginsDir, entry);
9565
9834
  let modulePath = null;
9566
9835
  try {
9567
9836
  const stat = statSync(entryPath);
9568
9837
  if (stat.isFile() && entry.endsWith(".js")) {
9569
9838
  modulePath = entryPath;
9570
9839
  } else if (stat.isDirectory()) {
9571
- const indexPath = join5(entryPath, "index.js");
9840
+ const indexPath = join4(entryPath, "index.js");
9572
9841
  if (existsSync6(indexPath)) {
9573
9842
  modulePath = indexPath;
9574
9843
  }
@@ -9581,7 +9850,7 @@ async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps) {
9581
9850
  }
9582
9851
  }
9583
9852
  await Promise.allSettled(
9584
- pluginPaths.filter(({ path }) => path.endsWith("index.js")).map(({ entry }) => ensurePluginDeps(join5(pluginsDir, entry), entry))
9853
+ pluginPaths.filter(({ path }) => path.endsWith("index.js")).map(({ entry }) => ensurePluginDeps(join4(pluginsDir, entry), entry))
9585
9854
  );
9586
9855
  const loadResults = await Promise.allSettled(
9587
9856
  pluginPaths.map(async ({ entry, path }) => {
@@ -9592,7 +9861,7 @@ async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps) {
9592
9861
  );
9593
9862
  for (const result of loadResults) {
9594
9863
  if (result.status === "rejected") {
9595
- log12.error(
9864
+ log10.error(
9596
9865
  `Plugin failed to load: ${result.reason instanceof Error ? result.reason.message : result.reason}`
9597
9866
  );
9598
9867
  continue;
@@ -9600,21 +9869,29 @@ async function loadEnhancedPlugins(config, loadedModuleNames, sdkDeps) {
9600
9869
  const { entry, mod } = result.value;
9601
9870
  try {
9602
9871
  if (!mod.tools || typeof mod.tools !== "function" && !Array.isArray(mod.tools)) {
9603
- log12.warn(`Plugin "${entry}": no 'tools' array or function exported, skipping`);
9872
+ log10.warn(`Plugin "${entry}": no 'tools' array or function exported, skipping`);
9604
9873
  continue;
9605
9874
  }
9606
- const adapted = adaptPlugin(mod, entry, config, loadedModuleNames, sdkDeps);
9875
+ const adapted = adaptPlugin(
9876
+ mod,
9877
+ entry,
9878
+ config,
9879
+ loadedModuleNames,
9880
+ sdkDeps,
9881
+ hookRegistry,
9882
+ pluginPriorities
9883
+ );
9607
9884
  if (loadedNames.has(adapted.name)) {
9608
- log12.warn(`Plugin "${adapted.name}" already loaded, skipping duplicate from "${entry}"`);
9885
+ log10.warn(`Plugin "${adapted.name}" already loaded, skipping duplicate from "${entry}"`);
9609
9886
  continue;
9610
9887
  }
9611
9888
  loadedNames.add(adapted.name);
9612
9889
  modules.push(adapted);
9613
9890
  } catch (err) {
9614
- log12.error(`Plugin "${entry}" failed to adapt: ${err instanceof Error ? err.message : err}`);
9891
+ log10.error(`Plugin "${entry}" failed to adapt: ${err instanceof Error ? err.message : err}`);
9615
9892
  }
9616
9893
  }
9617
- return modules;
9894
+ return { modules, hookRegistry };
9618
9895
  }
9619
9896
 
9620
9897
  // src/config/configurable-keys.ts
@@ -10122,6 +10399,40 @@ var CONFIGURABLE_KEYS = {
10122
10399
  mask: identity,
10123
10400
  parse: (v) => Number(v)
10124
10401
  },
10402
+ // ─── TON Proxy ────────────────────────────────────────────────────
10403
+ "ton_proxy.enabled": {
10404
+ type: "boolean",
10405
+ category: "TON Proxy",
10406
+ label: "TON Proxy Enabled",
10407
+ description: "Enable Tonutils-Proxy for .ton site access (auto-downloads binary on first run)",
10408
+ sensitive: false,
10409
+ hotReload: "instant",
10410
+ validate: enumValidator(["true", "false"]),
10411
+ mask: identity,
10412
+ parse: (v) => v === "true"
10413
+ },
10414
+ "ton_proxy.port": {
10415
+ type: "number",
10416
+ category: "TON Proxy",
10417
+ label: "Proxy Port",
10418
+ description: "HTTP proxy port for .ton sites (default: 8080)",
10419
+ sensitive: false,
10420
+ hotReload: "restart",
10421
+ validate: numberInRange(1, 65535),
10422
+ mask: identity,
10423
+ parse: (v) => Number(v)
10424
+ },
10425
+ "ton_proxy.binary_path": {
10426
+ type: "string",
10427
+ category: "TON Proxy",
10428
+ label: "Binary Path",
10429
+ description: "Custom path to tonutils-proxy-cli (leave empty for auto-download)",
10430
+ sensitive: false,
10431
+ hotReload: "restart",
10432
+ validate: noValidation,
10433
+ mask: identity,
10434
+ parse: identity
10435
+ },
10125
10436
  // ─── Capabilities ──────────────────────────────────────────────────
10126
10437
  "capabilities.exec.mode": {
10127
10438
  type: "enum",
@@ -10227,6 +10538,392 @@ function writeRawConfig(raw, configPath) {
10227
10538
  writeFileSync3(fullPath, stringify2(raw), { encoding: "utf-8", mode: 384 });
10228
10539
  }
10229
10540
 
10541
+ // src/ton-proxy/manager.ts
10542
+ import { spawn, execSync } from "child_process";
10543
+ import {
10544
+ existsSync as existsSync8,
10545
+ chmodSync,
10546
+ createWriteStream,
10547
+ readFileSync as readFileSync7,
10548
+ writeFileSync as writeFileSync4,
10549
+ unlinkSync
10550
+ } from "fs";
10551
+ import { mkdir } from "fs/promises";
10552
+ import { join as join5 } from "path";
10553
+ import { pipeline } from "stream/promises";
10554
+ var log11 = createLogger("TonProxy");
10555
+ var GITHUB_REPO = "xssnick/Tonutils-Proxy";
10556
+ var BINARY_DIR = join5(TELETON_ROOT, "bin");
10557
+ var PID_FILE = join5(TELETON_ROOT, "ton-proxy.pid");
10558
+ var HEALTH_CHECK_INTERVAL_MS = 3e4;
10559
+ var HEALTH_CHECK_TIMEOUT_MS = 5e3;
10560
+ var KILL_GRACE_MS = 5e3;
10561
+ var TonProxyManager = class {
10562
+ process = null;
10563
+ healthInterval = null;
10564
+ config;
10565
+ restartCount = 0;
10566
+ maxRestarts = 3;
10567
+ constructor(config) {
10568
+ this.config = config;
10569
+ }
10570
+ /** Resolve the binary path — user-specified or auto-detected */
10571
+ getBinaryPath() {
10572
+ if (this.config.binary_path) return this.config.binary_path;
10573
+ return join5(BINARY_DIR, getBinaryName());
10574
+ }
10575
+ /** Check if the binary exists on disk */
10576
+ isInstalled() {
10577
+ return existsSync8(this.getBinaryPath());
10578
+ }
10579
+ /** Whether the proxy process is currently running */
10580
+ isRunning() {
10581
+ return this.process !== null && this.process.exitCode === null;
10582
+ }
10583
+ /**
10584
+ * Download the latest CLI binary from GitHub releases.
10585
+ * Fetches the latest release tag, then downloads the platform-appropriate binary.
10586
+ */
10587
+ async install() {
10588
+ const binaryName = getBinaryName();
10589
+ log11.info(`Downloading TON Proxy binary (${binaryName})...`);
10590
+ await mkdir(BINARY_DIR, { recursive: true });
10591
+ const releaseUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
10592
+ const releaseRes = await fetch(releaseUrl, {
10593
+ headers: { Accept: "application/vnd.github.v3+json" }
10594
+ });
10595
+ if (!releaseRes.ok) {
10596
+ throw new Error(`Failed to fetch latest release: ${releaseRes.status}`);
10597
+ }
10598
+ const release = await releaseRes.json();
10599
+ const tag = release.tag_name;
10600
+ const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${binaryName}`;
10601
+ log11.info(`Downloading ${downloadUrl}`);
10602
+ const res = await fetch(downloadUrl);
10603
+ if (!res.ok || !res.body) {
10604
+ throw new Error(`Download failed: ${res.status} ${res.statusText}`);
10605
+ }
10606
+ const dest = this.getBinaryPath();
10607
+ const fileStream = createWriteStream(dest);
10608
+ await pipeline(res.body, fileStream);
10609
+ chmodSync(dest, 493);
10610
+ log11.info(`TON Proxy installed: ${dest} (${tag})`);
10611
+ }
10612
+ /** Kill any orphan proxy process from a previous session */
10613
+ killOrphan() {
10614
+ if (existsSync8(PID_FILE)) {
10615
+ try {
10616
+ const pid = parseInt(readFileSync7(PID_FILE, "utf-8").trim(), 10);
10617
+ if (pid && !isNaN(pid)) {
10618
+ try {
10619
+ process.kill(pid, 0);
10620
+ log11.warn(`Killing orphan TON Proxy (PID ${pid}) from previous session`);
10621
+ process.kill(pid, "SIGTERM");
10622
+ } catch {
10623
+ }
10624
+ }
10625
+ unlinkSync(PID_FILE);
10626
+ } catch {
10627
+ }
10628
+ }
10629
+ try {
10630
+ const out = execSync(`ss -tlnp 2>/dev/null | grep ':${this.config.port} ' || true`, {
10631
+ encoding: "utf-8",
10632
+ timeout: 3e3
10633
+ });
10634
+ const pidMatch = out.match(/pid=(\d+)/);
10635
+ if (pidMatch) {
10636
+ const pid = parseInt(pidMatch[1], 10);
10637
+ log11.warn(`Port ${this.config.port} occupied by PID ${pid}, killing it`);
10638
+ try {
10639
+ process.kill(pid, "SIGTERM");
10640
+ } catch {
10641
+ }
10642
+ execSync("sleep 0.5");
10643
+ }
10644
+ } catch {
10645
+ }
10646
+ }
10647
+ /** Write PID to file for orphan detection */
10648
+ writePidFile(pid) {
10649
+ try {
10650
+ writeFileSync4(PID_FILE, String(pid), { mode: 384 });
10651
+ } catch {
10652
+ log11.warn("Failed to write TON Proxy PID file");
10653
+ }
10654
+ }
10655
+ /** Remove PID file */
10656
+ removePidFile() {
10657
+ try {
10658
+ if (existsSync8(PID_FILE)) unlinkSync(PID_FILE);
10659
+ } catch {
10660
+ }
10661
+ }
10662
+ /** Start the proxy process */
10663
+ async start() {
10664
+ if (this.isRunning()) {
10665
+ log11.warn("TON Proxy is already running");
10666
+ return;
10667
+ }
10668
+ this.restartCount = 0;
10669
+ this.maxRestarts = 3;
10670
+ this.killOrphan();
10671
+ if (!this.isInstalled()) {
10672
+ await this.install();
10673
+ }
10674
+ const binaryPath = this.getBinaryPath();
10675
+ const port = String(this.config.port);
10676
+ log11.info(`Starting TON Proxy on 127.0.0.1:${port}`);
10677
+ this.process = spawn(binaryPath, ["-addr", `127.0.0.1:${port}`], {
10678
+ cwd: BINARY_DIR,
10679
+ stdio: ["ignore", "pipe", "pipe"],
10680
+ detached: false
10681
+ });
10682
+ this.process.stdout?.on("data", (chunk) => {
10683
+ const line = chunk.toString().trim();
10684
+ if (line) log11.debug(`[proxy] ${line}`);
10685
+ });
10686
+ this.process.stderr?.on("data", (chunk) => {
10687
+ const line = chunk.toString().trim();
10688
+ if (line) log11.warn(`[proxy:err] ${line}`);
10689
+ });
10690
+ this.process.on("exit", (code, signal) => {
10691
+ log11.info(`TON Proxy exited (code=${code}, signal=${signal})`);
10692
+ this.process = null;
10693
+ this.removePidFile();
10694
+ if (code !== 0 && code !== null && this.restartCount < this.maxRestarts) {
10695
+ this.restartCount++;
10696
+ log11.warn(`Auto-restarting TON Proxy (attempt ${this.restartCount}/${this.maxRestarts})`);
10697
+ this.start().catch((err) => log11.error({ err }, "Failed to auto-restart TON Proxy"));
10698
+ }
10699
+ });
10700
+ this.process.on("error", (err) => {
10701
+ log11.error({ err }, "TON Proxy process error");
10702
+ this.process = null;
10703
+ });
10704
+ this.startHealthCheck();
10705
+ await new Promise((resolve2, reject) => {
10706
+ const timer = setTimeout(() => {
10707
+ if (this.isRunning()) {
10708
+ resolve2();
10709
+ } else {
10710
+ reject(new Error("TON Proxy process exited immediately"));
10711
+ }
10712
+ }, 1e3);
10713
+ this.process?.on("exit", () => {
10714
+ clearTimeout(timer);
10715
+ reject(new Error("TON Proxy process exited during startup"));
10716
+ });
10717
+ });
10718
+ if (this.process?.pid) this.writePidFile(this.process.pid);
10719
+ log11.info(`TON Proxy running on 127.0.0.1:${port} (PID ${this.process?.pid})`);
10720
+ }
10721
+ /** Stop the proxy process gracefully */
10722
+ async stop() {
10723
+ this.stopHealthCheck();
10724
+ if (!this.process) return;
10725
+ this.maxRestarts = 0;
10726
+ log11.info("Stopping TON Proxy...");
10727
+ return new Promise((resolve2) => {
10728
+ if (!this.process) {
10729
+ resolve2();
10730
+ return;
10731
+ }
10732
+ const forceKill = setTimeout(() => {
10733
+ if (this.process) {
10734
+ log11.warn("TON Proxy did not exit gracefully, sending SIGKILL");
10735
+ this.process.kill("SIGKILL");
10736
+ }
10737
+ }, KILL_GRACE_MS);
10738
+ this.process.on("exit", () => {
10739
+ clearTimeout(forceKill);
10740
+ this.process = null;
10741
+ this.removePidFile();
10742
+ resolve2();
10743
+ });
10744
+ this.process.kill("SIGTERM");
10745
+ });
10746
+ }
10747
+ /** Remove the downloaded binary from disk */
10748
+ async uninstall() {
10749
+ if (this.isRunning()) {
10750
+ await this.stop();
10751
+ }
10752
+ const binaryPath = this.getBinaryPath();
10753
+ if (existsSync8(binaryPath)) {
10754
+ const { unlink } = await import("fs/promises");
10755
+ await unlink(binaryPath);
10756
+ log11.info(`TON Proxy binary removed: ${binaryPath}`);
10757
+ }
10758
+ }
10759
+ /** Get proxy status for WebUI / tools */
10760
+ getStatus() {
10761
+ return {
10762
+ running: this.isRunning(),
10763
+ port: this.config.port,
10764
+ installed: this.isInstalled(),
10765
+ pid: this.process?.pid
10766
+ };
10767
+ }
10768
+ startHealthCheck() {
10769
+ this.stopHealthCheck();
10770
+ this.healthInterval = setInterval(() => {
10771
+ void this.checkHealth();
10772
+ }, HEALTH_CHECK_INTERVAL_MS);
10773
+ }
10774
+ stopHealthCheck() {
10775
+ if (this.healthInterval) {
10776
+ clearInterval(this.healthInterval);
10777
+ this.healthInterval = null;
10778
+ }
10779
+ }
10780
+ async checkHealth() {
10781
+ if (!this.isRunning()) return;
10782
+ try {
10783
+ const controller = new AbortController();
10784
+ const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
10785
+ const res = await fetch(`http://127.0.0.1:${this.config.port}/`, {
10786
+ signal: controller.signal
10787
+ }).catch(() => null);
10788
+ clearTimeout(timeout);
10789
+ if (!res) {
10790
+ log11.warn("TON Proxy health check failed (no response)");
10791
+ }
10792
+ } catch {
10793
+ }
10794
+ }
10795
+ };
10796
+ function getBinaryName() {
10797
+ const platform = process.platform;
10798
+ const arch = process.arch;
10799
+ let os;
10800
+ switch (platform) {
10801
+ case "linux":
10802
+ os = "linux";
10803
+ break;
10804
+ case "darwin":
10805
+ os = "darwin";
10806
+ break;
10807
+ case "win32":
10808
+ os = "windows";
10809
+ break;
10810
+ default:
10811
+ throw new Error(`Unsupported platform: ${platform}`);
10812
+ }
10813
+ let cpuArch;
10814
+ switch (arch) {
10815
+ case "x64":
10816
+ cpuArch = "amd64";
10817
+ break;
10818
+ case "arm64":
10819
+ cpuArch = "arm64";
10820
+ break;
10821
+ default:
10822
+ throw new Error(`Unsupported architecture: ${arch}`);
10823
+ }
10824
+ const ext = platform === "win32" ? ".exe" : "";
10825
+ return `tonutils-proxy-cli-${os}-${cpuArch}${ext}`;
10826
+ }
10827
+
10828
+ // src/ton-proxy/tools.ts
10829
+ import { Type } from "@sinclair/typebox";
10830
+ var proxyManager = null;
10831
+ function setProxyManager(mgr) {
10832
+ proxyManager = mgr;
10833
+ }
10834
+ var tonProxyStatusTool = {
10835
+ name: "ton_proxy_status",
10836
+ description: "Check the status of the TON Proxy (Tonutils-Proxy). Returns whether the proxy is running, installed, the port, and PID.",
10837
+ parameters: Type.Object({})
10838
+ };
10839
+ var tonProxyStatusExecutor = async () => {
10840
+ if (!proxyManager) {
10841
+ return { success: true, data: { enabled: false, message: "TON Proxy is not configured" } };
10842
+ }
10843
+ return { success: true, data: proxyManager.getStatus() };
10844
+ };
10845
+
10846
+ // src/ton-proxy/module.ts
10847
+ var log12 = createLogger("TonProxyModule");
10848
+ var manager = null;
10849
+ function getTonProxyManager() {
10850
+ return manager;
10851
+ }
10852
+ function setTonProxyManager(mgr) {
10853
+ manager = mgr;
10854
+ setProxyManager(mgr);
10855
+ }
10856
+ var tonProxyModule = {
10857
+ name: "ton-proxy",
10858
+ version: "1.0.0",
10859
+ tools(config) {
10860
+ if (!config.ton_proxy?.enabled) return [];
10861
+ return [{ tool: tonProxyStatusTool, executor: tonProxyStatusExecutor }];
10862
+ },
10863
+ async start(context) {
10864
+ if (!context.config.ton_proxy?.enabled) return;
10865
+ const proxyConfig = context.config.ton_proxy;
10866
+ manager = new TonProxyManager({
10867
+ enabled: proxyConfig.enabled,
10868
+ port: proxyConfig.port,
10869
+ binary_path: proxyConfig.binary_path
10870
+ });
10871
+ setProxyManager(manager);
10872
+ try {
10873
+ await manager.start();
10874
+ log12.info(`TON Proxy started on port ${proxyConfig.port}`);
10875
+ } catch (err) {
10876
+ log12.error({ err }, "Failed to start TON Proxy");
10877
+ manager = null;
10878
+ }
10879
+ },
10880
+ async stop() {
10881
+ if (manager) {
10882
+ await manager.stop();
10883
+ manager = null;
10884
+ setProxyManager(null);
10885
+ }
10886
+ }
10887
+ };
10888
+ var module_default = tonProxyModule;
10889
+
10890
+ // src/agent/hooks/user-hook-store.ts
10891
+ function getUserHookConfig(db, key) {
10892
+ const row = db.prepare("SELECT value FROM user_hook_config WHERE key = ?").get(key);
10893
+ return row?.value ?? null;
10894
+ }
10895
+ function setUserHookConfig(db, key, value) {
10896
+ db.prepare(
10897
+ `INSERT INTO user_hook_config (key, value, updated_at)
10898
+ VALUES (?, ?, datetime('now'))
10899
+ ON CONFLICT(key) DO UPDATE SET
10900
+ value = excluded.value,
10901
+ updated_at = excluded.updated_at`
10902
+ ).run(key, value);
10903
+ }
10904
+ function getBlocklistConfig(db) {
10905
+ const enabled = getUserHookConfig(db, "blocklist.enabled");
10906
+ const keywords = getUserHookConfig(db, "blocklist.keywords");
10907
+ const message = getUserHookConfig(db, "blocklist.message");
10908
+ return {
10909
+ enabled: enabled === "true",
10910
+ keywords: keywords ? JSON.parse(keywords) : [],
10911
+ message: message ?? ""
10912
+ };
10913
+ }
10914
+ function setBlocklistConfig(db, config) {
10915
+ setUserHookConfig(db, "blocklist.enabled", String(config.enabled));
10916
+ setUserHookConfig(db, "blocklist.keywords", JSON.stringify(config.keywords));
10917
+ setUserHookConfig(db, "blocklist.message", config.message);
10918
+ }
10919
+ function getTriggersConfig(db) {
10920
+ const raw = getUserHookConfig(db, "triggers");
10921
+ return raw ? JSON.parse(raw) : [];
10922
+ }
10923
+ function setTriggersConfig(db, triggers) {
10924
+ setUserHookConfig(db, "triggers", JSON.stringify(triggers));
10925
+ }
10926
+
10230
10927
  export {
10231
10928
  loadConfig,
10232
10929
  configExists,
@@ -10268,6 +10965,9 @@ export {
10268
10965
  toUnits,
10269
10966
  fromUnits,
10270
10967
  dexFactory,
10968
+ getPluginPriorities,
10969
+ setPluginPriority,
10970
+ resetPluginPriority,
10271
10971
  InlineRouter,
10272
10972
  adaptPlugin,
10273
10973
  ensurePluginDeps,
@@ -10277,5 +10977,13 @@ export {
10277
10977
  setNestedValue,
10278
10978
  deleteNestedValue,
10279
10979
  readRawConfig,
10280
- writeRawConfig
10980
+ writeRawConfig,
10981
+ TonProxyManager,
10982
+ getTonProxyManager,
10983
+ setTonProxyManager,
10984
+ module_default,
10985
+ getBlocklistConfig,
10986
+ setBlocklistConfig,
10987
+ getTriggersConfig,
10988
+ setTriggersConfig
10281
10989
  };