@wabot-dev/framework 0.9.22 → 0.9.24

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 (26) hide show
  1. package/dist/src/addon/chat-bot/anthropic/AnthropicChatAdapter.js +9 -6
  2. package/dist/src/addon/chat-bot/deepseek/DeepSeekChatAdapter.js +1 -0
  3. package/dist/src/addon/chat-bot/google/GoogleChatAdapter.js +8 -5
  4. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -0
  5. package/dist/src/addon/chat-bot/in-memory/InMemoryChatRepository.js +1 -0
  6. package/dist/src/addon/chat-bot/openia/OpenaiChatAdapter.js +9 -6
  7. package/dist/src/addon/chat-bot/openrouter/OpenRouterChatAdapter.js +9 -6
  8. package/dist/src/addon/chat-bot/pg/PgChatMemory.js +1 -0
  9. package/dist/src/addon/chat-bot/pg/PgChatRepository.js +1 -0
  10. package/dist/src/addon/chat-bot/wabot/WabotChatAdapter.js +1 -0
  11. package/dist/src/addon/chat-controller/cmd/CmdChannel.js +12 -2
  12. package/dist/src/addon/chat-controller/cmd/CmdChannelServer.js +1 -1
  13. package/dist/src/addon/chat-controller/cmd/cmdClientImages.js +132 -0
  14. package/dist/src/addon/chat-controller/cmd/runCmdClient.js +99 -6
  15. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +43 -1
  16. package/dist/src/feature/chat-bot/ChatBot.js +30 -7
  17. package/dist/src/feature/chat-bot/ImageDescriber.js +95 -0
  18. package/dist/src/feature/chat-bot/extractChatMessageText.js +1 -0
  19. package/dist/src/feature/chat-bot/pendingMediaStartIndex.js +22 -0
  20. package/dist/src/feature/chat-bot/unconsumedMediaStartIndex.js +27 -0
  21. package/dist/src/feature/chat-controller/ChatResolver.js +1 -0
  22. package/dist/src/feature/chat-controller/runChatControllers.js +1 -0
  23. package/dist/src/feature/project-runner/ProjectRunner.js +1 -0
  24. package/dist/src/index.d.ts +117 -35
  25. package/dist/src/index.js +3 -0
  26. package/package.json +2 -2
@@ -7,6 +7,7 @@ import '../../../feature/chat-bot/ChatBot.js';
7
7
  import '../../../feature/chat-bot/ChatOperator.js';
8
8
  import '../../../feature/chat-bot/UnionChatAdapter.js';
9
9
  import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
10
+ import '../../../feature/chat-bot/ImageDescriber.js';
10
11
  import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
11
12
  import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
12
13
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
@@ -14,6 +15,7 @@ import 'uuid';
14
15
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
16
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
16
17
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
18
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
17
19
  import { Anthropic } from '@anthropic-ai/sdk';
18
20
 
19
21
  const ANTHROPIC_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -60,10 +62,11 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
60
62
  }
61
63
  mapChatItems(chatItems) {
62
64
  const messages = [];
63
- for (const chatItem of chatItems) {
65
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
66
+ chatItems.forEach((chatItem, index) => {
64
67
  switch (chatItem.type) {
65
68
  case 'humanMessage':
66
- messages.push(this.mapHumanMessage(chatItem.humanMessage));
69
+ messages.push(this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
67
70
  break;
68
71
  case 'botMessage':
69
72
  messages.push(this.mapBotMessage(chatItem.botMessage));
@@ -72,10 +75,10 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
72
75
  messages.push(...this.mapFunctionCall(chatItem.functionCall));
73
76
  break;
74
77
  }
75
- }
78
+ });
76
79
  return messages;
77
80
  }
78
- mapHumanMessage(item) {
81
+ mapHumanMessage(item, includeMedia) {
79
82
  if (isChatMessageEmpty(item)) {
80
83
  throw new Error('User message content is empty');
81
84
  }
@@ -87,14 +90,14 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
87
90
  supportedDocumentMimeTypes: ANTHROPIC_SUPPORTED_DOCUMENT_MIME_TYPES,
88
91
  }),
89
92
  });
90
- if (item.images) {
93
+ if (includeMedia && item.images) {
91
94
  for (const image of item.images) {
92
95
  if (!ANTHROPIC_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
93
96
  continue;
94
97
  blocks.push({ type: 'image', source: this.toAnthropicImageSource(image) });
95
98
  }
96
99
  }
97
- if (item.documents) {
100
+ if (includeMedia && item.documents) {
98
101
  for (const doc of item.documents) {
99
102
  if (!ANTHROPIC_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
100
103
  continue;
@@ -6,6 +6,7 @@ import '../../../feature/chat-bot/ChatBot.js';
6
6
  import '../../../feature/chat-bot/ChatOperator.js';
7
7
  import '../../../feature/chat-bot/UnionChatAdapter.js';
8
8
  import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
9
+ import '../../../feature/chat-bot/ImageDescriber.js';
9
10
  import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
10
11
  import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
11
12
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
@@ -8,6 +8,7 @@ import '../../../feature/chat-bot/ChatBot.js';
8
8
  import '../../../feature/chat-bot/ChatOperator.js';
9
9
  import '../../../feature/chat-bot/UnionChatAdapter.js';
10
10
  import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
11
+ import '../../../feature/chat-bot/ImageDescriber.js';
11
12
  import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
12
13
  import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
13
14
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
@@ -15,6 +16,7 @@ import 'uuid';
15
16
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
16
17
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
17
18
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
19
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
18
20
  import { GoogleGenAI } from '@google/genai';
19
21
 
20
22
  const GOOGLE_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -82,10 +84,11 @@ let GoogleChatAdapter = class GoogleChatAdapter {
82
84
  }
83
85
  async mapChatItems(chatItems) {
84
86
  const contents = [];
85
- for (const chatItem of chatItems) {
87
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
88
+ for (const [index, chatItem] of chatItems.entries()) {
86
89
  switch (chatItem.type) {
87
90
  case 'humanMessage':
88
- contents.push(await this.mapHumanMessage(chatItem.humanMessage));
91
+ contents.push(await this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
89
92
  break;
90
93
  case 'botMessage':
91
94
  contents.push(this.mapBotMessage(chatItem.botMessage));
@@ -97,7 +100,7 @@ let GoogleChatAdapter = class GoogleChatAdapter {
97
100
  }
98
101
  return contents;
99
102
  }
100
- async mapHumanMessage(item) {
103
+ async mapHumanMessage(item, includeMedia) {
101
104
  if (isChatMessageEmpty(item)) {
102
105
  throw new Error('User message content is empty');
103
106
  }
@@ -109,14 +112,14 @@ let GoogleChatAdapter = class GoogleChatAdapter {
109
112
  }),
110
113
  });
111
114
  const filesToSend = [];
112
- if (item.images) {
115
+ if (includeMedia && item.images) {
113
116
  for (const image of item.images) {
114
117
  if (!GOOGLE_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
115
118
  continue;
116
119
  filesToSend.push(image);
117
120
  }
118
121
  }
119
- if (item.documents) {
122
+ if (includeMedia && item.documents) {
120
123
  for (const doc of item.documents) {
121
124
  if (!GOOGLE_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
122
125
  continue;
@@ -6,6 +6,7 @@ import '../../../feature/chat-bot/ChatBot.js';
6
6
  import { ChatItem } from '../../../feature/chat-bot/ChatItem.js';
7
7
  import '../../../feature/chat-bot/ChatOperator.js';
8
8
  import '../../../feature/chat-bot/UnionChatAdapter.js';
9
+ import '../../../feature/chat-bot/ImageDescriber.js';
9
10
  import '../../../core/injection/index.js';
10
11
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
11
12
  import 'uuid';
@@ -10,6 +10,7 @@ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
10
10
  import '../../../feature/chat-bot/ChatBot.js';
11
11
  import { ChatOperator } from '../../../feature/chat-bot/ChatOperator.js';
12
12
  import '../../../feature/chat-bot/UnionChatAdapter.js';
13
+ import '../../../feature/chat-bot/ImageDescriber.js';
13
14
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
14
15
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
16
  import '../../../core/error/setupErrorHandlers.js';
@@ -4,6 +4,7 @@ import '../../../feature/chat-bot/ChatBot.js';
4
4
  import '../../../feature/chat-bot/ChatOperator.js';
5
5
  import '../../../feature/chat-bot/UnionChatAdapter.js';
6
6
  import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
7
+ import '../../../feature/chat-bot/ImageDescriber.js';
7
8
  import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
8
9
  import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
9
10
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
@@ -12,6 +13,7 @@ import 'uuid';
12
13
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
13
14
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
14
15
  import '../../../core/error/setupErrorHandlers.js';
16
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
15
17
  import { Logger } from '../../../core/logger/Logger.js';
16
18
  import { OpenAI } from 'openai';
17
19
 
@@ -51,10 +53,11 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
51
53
  }
52
54
  mapChatItems(chatItems) {
53
55
  const openIaInput = [];
54
- for (const chatItem of chatItems) {
56
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
57
+ chatItems.forEach((chatItem, index) => {
55
58
  switch (chatItem.type) {
56
59
  case 'humanMessage':
57
- openIaInput.push(this.mapConectionMessage(chatItem.humanMessage));
60
+ openIaInput.push(this.mapConectionMessage(chatItem.humanMessage, index >= mediaStart));
58
61
  break;
59
62
  case 'botMessage':
60
63
  openIaInput.push(this.mapBotMessage(chatItem.botMessage));
@@ -63,10 +66,10 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
63
66
  openIaInput.push(...this.mapFunctionCall(chatItem.functionCall));
64
67
  break;
65
68
  }
66
- }
69
+ });
67
70
  return openIaInput;
68
71
  }
69
- mapConectionMessage(item) {
72
+ mapConectionMessage(item, includeMedia) {
70
73
  if (isChatMessageEmpty(item)) {
71
74
  throw new Error('User message content is empty');
72
75
  }
@@ -78,7 +81,7 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
78
81
  supportedDocumentMimeTypes: OPENAI_SUPPORTED_DOCUMENT_MIME_TYPES,
79
82
  }),
80
83
  });
81
- if (item.images) {
84
+ if (includeMedia && item.images) {
82
85
  for (const image of item.images) {
83
86
  if (!OPENAI_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
84
87
  continue;
@@ -89,7 +92,7 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
89
92
  });
90
93
  }
91
94
  }
92
- if (item.documents) {
95
+ if (includeMedia && item.documents) {
93
96
  for (const doc of item.documents) {
94
97
  if (!OPENAI_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
95
98
  continue;
@@ -7,6 +7,7 @@ import '../../../feature/chat-bot/ChatBot.js';
7
7
  import '../../../feature/chat-bot/ChatOperator.js';
8
8
  import '../../../feature/chat-bot/UnionChatAdapter.js';
9
9
  import { extractChatMessageText } from '../../../feature/chat-bot/extractChatMessageText.js';
10
+ import '../../../feature/chat-bot/ImageDescriber.js';
10
11
  import { isChatMessageEmpty } from '../../../feature/chat-bot/isChatMessageEmpty.js';
11
12
  import { isRetryableError } from '../../../feature/chat-bot/isRetryableError.js';
12
13
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
@@ -14,6 +15,7 @@ import 'uuid';
14
15
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
16
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
16
17
  import '../../../core/error/setupErrorHandlers.js';
18
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
17
19
  import { OpenRouter } from '@openrouter/sdk';
18
20
 
19
21
  const OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -66,10 +68,11 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
66
68
  }
67
69
  mapChatItems(chatItems) {
68
70
  const messages = [];
69
- for (const chatItem of chatItems) {
71
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
72
+ chatItems.forEach((chatItem, index) => {
70
73
  switch (chatItem.type) {
71
74
  case 'humanMessage':
72
- messages.push(this.mapHumanMessage(chatItem.humanMessage));
75
+ messages.push(this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
73
76
  break;
74
77
  case 'botMessage':
75
78
  messages.push(this.mapBotMessage(chatItem.botMessage));
@@ -78,10 +81,10 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
78
81
  messages.push(...this.mapFunctionCall(chatItem.functionCall));
79
82
  break;
80
83
  }
81
- }
84
+ });
82
85
  return messages;
83
86
  }
84
- mapHumanMessage(item) {
87
+ mapHumanMessage(item, includeMedia) {
85
88
  if (isChatMessageEmpty(item)) {
86
89
  throw new Error('User message content is empty');
87
90
  }
@@ -90,7 +93,7 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
90
93
  supportedImageMimeTypes: OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES,
91
94
  supportedDocumentMimeTypes: OPENROUTER_SUPPORTED_DOCUMENT_MIME_TYPES,
92
95
  }));
93
- if (item.images) {
96
+ if (includeMedia && item.images) {
94
97
  for (const image of item.images) {
95
98
  if (!OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
96
99
  continue;
@@ -99,7 +102,7 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
99
102
  contentParts.push(imageUrl);
100
103
  }
101
104
  }
102
- if (item.documents) {
105
+ if (includeMedia && item.documents) {
103
106
  for (const doc of item.documents) {
104
107
  if (!OPENROUTER_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
105
108
  continue;
@@ -13,6 +13,7 @@ import '../../../feature/chat-bot/ChatBot.js';
13
13
  import { ChatItem } from '../../../feature/chat-bot/ChatItem.js';
14
14
  import '../../../feature/chat-bot/ChatOperator.js';
15
15
  import '../../../feature/chat-bot/UnionChatAdapter.js';
16
+ import '../../../feature/chat-bot/ImageDescriber.js';
16
17
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
17
18
  import 'uuid';
18
19
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
@@ -16,6 +16,7 @@ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
16
16
  import '../../../feature/chat-bot/ChatBot.js';
17
17
  import { ChatOperator } from '../../../feature/chat-bot/ChatOperator.js';
18
18
  import '../../../feature/chat-bot/UnionChatAdapter.js';
19
+ import '../../../feature/chat-bot/ImageDescriber.js';
19
20
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
20
21
  import 'uuid';
21
22
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
@@ -8,6 +8,7 @@ import '../../../feature/chat-bot/ChatAdapterRegistry.js';
8
8
  import '../../../feature/chat-bot/ChatBot.js';
9
9
  import '../../../feature/chat-bot/ChatOperator.js';
10
10
  import '../../../feature/chat-bot/UnionChatAdapter.js';
11
+ import '../../../feature/chat-bot/ImageDescriber.js';
11
12
  import { chatAdapter } from '../../../feature/chat-bot/metadata/@chatAdapter.js';
12
13
  import 'uuid';
13
14
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
@@ -33,7 +33,7 @@ let CmdChannel = class CmdChannel {
33
33
  }
34
34
  connect() {
35
35
  this.server.register(this.config.route, {
36
- onMessage: async (text, reply) => {
36
+ onMessage: async ({ text, images }, reply) => {
37
37
  if (!this.callBack)
38
38
  return;
39
39
  this.ensureChatId();
@@ -46,7 +46,7 @@ let CmdChannel = class CmdChannel {
46
46
  await this.callBack({
47
47
  channel: cmdChannelName,
48
48
  chatConnection,
49
- message: { text },
49
+ message: { text, images: toChatImages(images) },
50
50
  reply: async (message) => {
51
51
  reply({
52
52
  senderName: message.senderName,
@@ -98,6 +98,16 @@ CmdChannel = CmdChannel_1 = __decorate([
98
98
  CmdChannelServer,
99
99
  CmdChannelConfig])
100
100
  ], CmdChannel);
101
+ function toChatImages(images) {
102
+ if (!images || images.length === 0)
103
+ return undefined;
104
+ return images.map((image) => ({
105
+ id: Random.alphaNumericLowerCase(10),
106
+ name: image.name,
107
+ mimeType: image.mimeType,
108
+ base64Url: image.base64Url,
109
+ }));
110
+ }
101
111
  function extractDisplayText(message) {
102
112
  const raw = message.text ?? '';
103
113
  const trimmed = raw.trim();
@@ -146,7 +146,7 @@ let CmdChannelServer = class CmdChannelServer {
146
146
  this.activeRoute = null;
147
147
  return;
148
148
  }
149
- await handlers.onMessage(msg.text, (reply) => {
149
+ await handlers.onMessage({ text: msg.text, images: msg.images }, (reply) => {
150
150
  this.sendToClient({ type: 'reply', ...reply });
151
151
  });
152
152
  return;
@@ -0,0 +1,132 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+
6
+ const IMAGE_MIME_BY_EXT = {
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.gif': 'image/gif',
11
+ '.webp': 'image/webp',
12
+ '.heic': 'image/heic',
13
+ '.heif': 'image/heif',
14
+ '.bmp': 'image/bmp',
15
+ };
16
+ /** Image mime type for a file path based on its extension, or null if not an image. */
17
+ function imageMimeForPath(filePath) {
18
+ return IMAGE_MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? null;
19
+ }
20
+ /**
21
+ * Normalizes a path as a terminal hands it over on drag-and-drop or paste:
22
+ * strips surrounding single/double quotes and unescapes backslash-escaped
23
+ * characters (e.g. `\ ` for spaces).
24
+ */
25
+ function parseDroppedPath(input) {
26
+ let value = input.trim();
27
+ if (value.length >= 2 &&
28
+ ((value.startsWith("'") && value.endsWith("'")) ||
29
+ (value.startsWith('"') && value.endsWith('"')))) {
30
+ value = value.slice(1, -1);
31
+ }
32
+ else {
33
+ value = value.replace(/\\(.)/g, '$1');
34
+ }
35
+ return value;
36
+ }
37
+ /** Reads an image file into a wire image, or returns null if it is not a readable image file. */
38
+ function imageFromPath(rawPath) {
39
+ const filePath = parseDroppedPath(rawPath);
40
+ const mimeType = imageMimeForPath(filePath);
41
+ if (!mimeType)
42
+ return null;
43
+ try {
44
+ if (!fs.statSync(filePath).isFile())
45
+ return null;
46
+ const base64 = fs.readFileSync(filePath).toString('base64');
47
+ return {
48
+ name: path.basename(filePath),
49
+ mimeType,
50
+ base64Url: `data:${mimeType};base64,${base64}`,
51
+ };
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Reads an image from the system clipboard, cross-platform. Returns null when
59
+ * the clipboard holds no image or the platform tool is unavailable.
60
+ *
61
+ * - macOS: `osascript` (built in)
62
+ * - Linux: `wl-paste` (Wayland) then `xclip` (X11)
63
+ * - Windows: PowerShell `System.Windows.Forms.Clipboard`
64
+ */
65
+ function readClipboardImage() {
66
+ const tmpFile = path.join(os.tmpdir(), `wabot-clip-${process.pid}-${Date.now()}.png`);
67
+ try {
68
+ if (!dumpClipboardImage(tmpFile))
69
+ return null;
70
+ const stat = fs.statSync(tmpFile);
71
+ if (!stat.isFile() || stat.size === 0)
72
+ return null;
73
+ const base64 = fs.readFileSync(tmpFile).toString('base64');
74
+ return {
75
+ name: 'clipboard.png',
76
+ mimeType: 'image/png',
77
+ base64Url: `data:image/png;base64,${base64}`,
78
+ };
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ finally {
84
+ try {
85
+ fs.unlinkSync(tmpFile);
86
+ }
87
+ catch {
88
+ // ignore: file may not have been created
89
+ }
90
+ }
91
+ }
92
+ function dumpClipboardImage(outFile) {
93
+ switch (process.platform) {
94
+ case 'darwin': {
95
+ const script = [
96
+ `set theFile to (open for access (POSIX file ${JSON.stringify(outFile)}) with write permission)`,
97
+ 'try',
98
+ ' set eof theFile to 0',
99
+ ' write (the clipboard as «class PNGf») to theFile',
100
+ ' close access theFile',
101
+ 'on error',
102
+ ' close access theFile',
103
+ ' error "no image on clipboard"',
104
+ 'end try',
105
+ ].join('\n');
106
+ return run('osascript', ['-e', script]);
107
+ }
108
+ case 'linux': {
109
+ const quoted = `'${outFile.replace(/'/g, `'\\''`)}'`;
110
+ return (run('sh', ['-c', `wl-paste --type image/png > ${quoted}`]) ||
111
+ run('sh', ['-c', `xclip -selection clipboard -t image/png -o > ${quoted}`]));
112
+ }
113
+ case 'win32': {
114
+ const ps = 'Add-Type -AssemblyName System.Windows.Forms;' +
115
+ '$img=[Windows.Forms.Clipboard]::GetImage();' +
116
+ `if($img -ne $null){$img.Save(${JSON.stringify(outFile)});exit 0}else{exit 1}`;
117
+ return run('powershell', ['-NoProfile', '-Command', ps]);
118
+ }
119
+ default:
120
+ return false;
121
+ }
122
+ }
123
+ function run(command, args) {
124
+ try {
125
+ return spawnSync(command, args, { stdio: ['ignore', 'ignore', 'ignore'] }).status === 0;
126
+ }
127
+ catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ export { imageFromPath, imageMimeForPath, parseDroppedPath, readClipboardImage };
@@ -1,6 +1,7 @@
1
1
  import * as net from 'node:net';
2
2
  import * as readline from 'node:readline';
3
3
  import { cmdChannelSocketPath } from './cmdChannelSocketPath.js';
4
+ import { readClipboardImage, imageFromPath } from './cmdClientImages.js';
4
5
 
5
6
  const useColor = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
6
7
  const ansi = (code) => (text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
@@ -11,11 +12,13 @@ const green = ansi('1;32');
11
12
  const greenText = ansi('32');
12
13
  const red = ansi('1;31');
13
14
  const yellow = ansi('33');
14
- const COMMANDS = ['/channels', '/clear', '/help', '/exit'];
15
+ const COMMANDS = ['/channels', '/clear', '/help', '/image', '/paste', '/exit'];
15
16
  const HELP_LINES = [
16
17
  'Commands:',
17
18
  ' /channels list channels and switch',
18
19
  ' /clear start a fresh conversation on the current channel',
20
+ ' /image <p> attach an image file (or drag a file into the terminal)',
21
+ ' /paste attach an image from the clipboard (or press Ctrl+V)',
19
22
  ' /help show this help',
20
23
  ' /exit quit',
21
24
  ];
@@ -31,6 +34,11 @@ function runCmdClient() {
31
34
  let waitingNoticeShown = false;
32
35
  let exiting = false;
33
36
  let restoring = false;
37
+ let pendingImages = [];
38
+ const chattingPrompt = (route) => {
39
+ const tag = pendingImages.length > 0 ? yellow(` [${pendingImages.length} img]`) : '';
40
+ return cyan(route) + tag + dim(' > ');
41
+ };
34
42
  const completer = (line) => {
35
43
  if (line.startsWith('/')) {
36
44
  const hits = COMMANDS.filter((c) => c.startsWith(line));
@@ -58,6 +66,41 @@ function runCmdClient() {
58
66
  socket.write(JSON.stringify(msg) + '\n');
59
67
  return true;
60
68
  };
69
+ const refreshChatPrompt = () => {
70
+ if (state === 'chatting' && selected)
71
+ rl.setPrompt(chattingPrompt(selected));
72
+ };
73
+ const attachImage = (image, label) => {
74
+ pendingImages.push(image);
75
+ process.stdout.write(green(`[attached ${label}]`) + dim(' — press Enter to send, or type a caption first') + '\n');
76
+ refreshChatPrompt();
77
+ rl.prompt();
78
+ };
79
+ const attachFromClipboard = () => {
80
+ if (state !== 'chatting') {
81
+ process.stderr.write(red('select a channel before pasting an image.') + '\n');
82
+ rl.prompt();
83
+ return;
84
+ }
85
+ const image = readClipboardImage();
86
+ if (!image) {
87
+ process.stdout.write(yellow('No image found on the clipboard.') + '\n');
88
+ rl.prompt();
89
+ return;
90
+ }
91
+ attachImage(image, 'clipboard image');
92
+ };
93
+ const sendChatMessage = (text) => {
94
+ const images = pendingImages.length > 0 ? pendingImages : undefined;
95
+ if (!text && !images) {
96
+ rl.prompt();
97
+ return;
98
+ }
99
+ if (send({ type: 'message', text: text || undefined, images })) {
100
+ pendingImages = [];
101
+ refreshChatPrompt();
102
+ }
103
+ };
61
104
  const printChannels = (list) => {
62
105
  routes = list;
63
106
  if (list.length === 0) {
@@ -90,7 +133,7 @@ function runCmdClient() {
90
133
  process.stdout.write(green(`[connected to ${msg.route}]`) + '\n');
91
134
  }
92
135
  restoring = false;
93
- rl.setPrompt(cyan(msg.route) + dim(' > '));
136
+ rl.setPrompt(chattingPrompt(msg.route));
94
137
  rl.prompt();
95
138
  return;
96
139
  case 'reply':
@@ -128,7 +171,7 @@ function runCmdClient() {
128
171
  restoring = true;
129
172
  socket.write(JSON.stringify({ type: 'select', route: selected }) + '\n');
130
173
  state = 'chatting';
131
- rl.setPrompt(cyan(selected) + dim(' > '));
174
+ rl.setPrompt(chattingPrompt(selected));
132
175
  rl.prompt();
133
176
  }
134
177
  else {
@@ -188,10 +231,15 @@ function runCmdClient() {
188
231
  socket.end();
189
232
  process.exit(code);
190
233
  };
191
- rl.on('line', (input) => {
234
+ rl.on('line', (rawInput) => {
235
+ // Strip stray control chars (e.g. a ^V left behind by the Ctrl+V shortcut).
236
+ const input = rawInput.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
192
237
  const trimmed = input.trim();
193
238
  if (!trimmed) {
194
- rl.prompt();
239
+ if (state === 'chatting' && pendingImages.length > 0)
240
+ sendChatMessage('');
241
+ else
242
+ rl.prompt();
195
243
  return;
196
244
  }
197
245
  if (trimmed === '/exit' || trimmed.toLowerCase() === 'exit') {
@@ -213,9 +261,36 @@ function runCmdClient() {
213
261
  rl.prompt();
214
262
  return;
215
263
  }
264
+ pendingImages = [];
265
+ refreshChatPrompt();
216
266
  send({ type: 'clear' });
217
267
  return;
218
268
  }
269
+ if (trimmed === '/paste') {
270
+ attachFromClipboard();
271
+ return;
272
+ }
273
+ if (trimmed === '/image' || trimmed.startsWith('/image ')) {
274
+ if (state !== 'chatting') {
275
+ process.stderr.write(red('select a channel before attaching an image.') + '\n');
276
+ rl.prompt();
277
+ return;
278
+ }
279
+ const rawPath = trimmed.slice('/image'.length).trim();
280
+ if (!rawPath) {
281
+ process.stderr.write(red('usage: /image <path-to-image>') + '\n');
282
+ rl.prompt();
283
+ return;
284
+ }
285
+ const image = imageFromPath(rawPath);
286
+ if (!image) {
287
+ process.stderr.write(red(`not a readable image file: ${rawPath}`) + '\n');
288
+ rl.prompt();
289
+ return;
290
+ }
291
+ attachImage(image, image.name ?? 'image');
292
+ return;
293
+ }
219
294
  if (state === 'disconnected') {
220
295
  process.stderr.write(red('not connected to framework — waiting for server...') + '\n');
221
296
  rl.prompt();
@@ -231,9 +306,27 @@ function runCmdClient() {
231
306
  send({ type: 'select', route: routes[num - 1].route });
232
307
  return;
233
308
  }
234
- send({ type: 'message', text: trimmed });
309
+ // A bare path to an image file — from drag-and-drop, or a terminal that
310
+ // pastes a file path on Cmd+V — attaches instead of being sent as text.
311
+ const dropped = imageFromPath(trimmed);
312
+ if (dropped) {
313
+ attachImage(dropped, dropped.name ?? 'image');
314
+ return;
315
+ }
316
+ sendChatMessage(trimmed);
235
317
  });
236
318
  rl.on('close', () => cleanup(0));
319
+ // Ctrl+V pastes a clipboard image. (Cmd+V is handled by the terminal itself,
320
+ // which pastes text/a file path — the latter is picked up as a dropped path.)
321
+ if (process.stdin.isTTY) {
322
+ readline.emitKeypressEvents(process.stdin);
323
+ process.stdin.on('keypress', (_str, key) => {
324
+ if (key && key.ctrl && key.name === 'v') {
325
+ process.stdout.write('\n');
326
+ attachFromClipboard();
327
+ }
328
+ });
329
+ }
237
330
  connect();
238
331
  }
239
332
 
@@ -2,6 +2,7 @@ import { __decorate, __metadata } from 'tslib';
2
2
  import { Bot } from 'grammy';
3
3
  import { TelegramChannelConfig } from './TelegramChannelConfig.js';
4
4
  import { injectable } from '../../../core/injection/index.js';
5
+ import { Logger } from '../../../core/logger/Logger.js';
5
6
  import { markdownToTelegramHtml } from './markdownToTelegramHtml.js';
6
7
  import { telegramChannelName } from './telegramChannelName.js';
7
8
 
@@ -11,6 +12,7 @@ let TelegramChannel = class TelegramChannel {
11
12
  config;
12
13
  static channelName = telegramChannelName;
13
14
  bot;
15
+ logger = new Logger('wabot:telegram-channel');
14
16
  constructor(config) {
15
17
  this.config = config;
16
18
  this.bot = new Bot(this.config.botToken);
@@ -25,13 +27,16 @@ let TelegramChannel = class TelegramChannel {
25
27
  chatType: ctx.message.chat.type === 'private' ? 'PRIVATE' : 'GROUP',
26
28
  channelName: TelegramChannel_1.channelName,
27
29
  };
30
+ const { images, documents } = await this.extractMedia(ctx.api, ctx.message);
28
31
  await callback({
29
32
  channel: telegramChannelName,
30
33
  chatConnection,
31
34
  message: {
32
35
  senderId: ctx.from.id.toString(),
33
36
  senderName: ctx.from.first_name,
34
- text: ctx.message.text,
37
+ text: ctx.message.text ?? ctx.message.caption,
38
+ images: images.length > 0 ? images : undefined,
39
+ documents: documents.length > 0 ? documents : undefined,
35
40
  },
36
41
  reply: async (replyMessage) => {
37
42
  if (!replyMessage.text)
@@ -43,6 +48,43 @@ let TelegramChannel = class TelegramChannel {
43
48
  });
44
49
  });
45
50
  }
51
+ async extractMedia(api, message) {
52
+ const images = [];
53
+ const documents = [];
54
+ if (message.photo && message.photo.length > 0) {
55
+ // Photos arrive in multiple sizes; the last entry is the highest resolution.
56
+ const largest = message.photo[message.photo.length - 1];
57
+ const file = await this.downloadChatFile(api, largest.file_id, largest.file_unique_id, 'image/jpeg');
58
+ if (file)
59
+ images.push(file);
60
+ }
61
+ if (message.document) {
62
+ const doc = message.document;
63
+ const mimeType = doc.mime_type ?? 'application/octet-stream';
64
+ const file = await this.downloadChatFile(api, doc.file_id, doc.file_unique_id, mimeType, doc.file_name);
65
+ // An image sent as an uncompressed file still belongs with the images.
66
+ if (file)
67
+ (mimeType.startsWith('image/') ? images : documents).push(file);
68
+ }
69
+ return { images, documents };
70
+ }
71
+ async downloadChatFile(api, fileId, id, mimeType, name) {
72
+ try {
73
+ const file = await api.getFile(fileId);
74
+ if (!file.file_path)
75
+ return null;
76
+ const url = `https://api.telegram.org/file/bot${this.config.botToken}/${file.file_path}`;
77
+ const res = await fetch(url);
78
+ if (!res.ok)
79
+ throw new Error(`${res.status} ${res.statusText}`);
80
+ const base64 = Buffer.from(await res.arrayBuffer()).toString('base64');
81
+ return { id, name, mimeType, base64Url: `data:${mimeType};base64,${base64}` };
82
+ }
83
+ catch (err) {
84
+ this.logger.warn(`failed to download telegram file '${id}'`, err instanceof Error ? { message: err.message } : { err });
85
+ return null;
86
+ }
87
+ }
46
88
  connect() {
47
89
  this.bot.start();
48
90
  }
@@ -5,6 +5,8 @@ import { MindsetOperator } from '../mindset/MindsetOperator.js';
5
5
  import { ChatAdapter } from './ChatAdapter.js';
6
6
  import { ChatItem } from './ChatItem.js';
7
7
  import { ChatMemory } from './ChatMemory.js';
8
+ import { ImageDescriber } from './ImageDescriber.js';
9
+ import { pendingMediaStartIndex } from './pendingMediaStartIndex.js';
8
10
  import { Logger } from '../../core/logger/Logger.js';
9
11
 
10
12
  const MAX_CONSECUTIVE_INVALID_ARGS = 2;
@@ -18,13 +20,16 @@ let ChatBot = class ChatBot {
18
20
  memory;
19
21
  adapter;
20
22
  mindset;
23
+ imageDescriber;
21
24
  logger = new Logger('wabot:chat-bot');
22
- constructor(memory, adapter, mindset) {
25
+ constructor(memory, adapter, mindset, imageDescriber) {
23
26
  this.memory = memory;
24
27
  this.adapter = adapter;
25
28
  this.mindset = mindset;
29
+ this.imageDescriber = imageDescriber;
26
30
  }
27
31
  async sendMessage(message, callback) {
32
+ await this.describeImages(message);
28
33
  const newChatItem = new ChatItem({
29
34
  type: 'humanMessage',
30
35
  humanMessage: message,
@@ -32,6 +37,20 @@ let ChatBot = class ChatBot {
32
37
  await this.memory.create(newChatItem);
33
38
  await this.processLoop(callback, 0);
34
39
  }
40
+ // Caption incoming images once, before persisting, so later turns can recall
41
+ // them from the stored description instead of re-sending the binary. The
42
+ // mindset's context/skills/limits focus the description on what matters here.
43
+ async describeImages(message) {
44
+ if (!message.images?.some((image) => !image.description))
45
+ return;
46
+ const [models, context, skills, limits] = await Promise.all([
47
+ this.mindset.resolveModels('visionLlm'),
48
+ this.mindset.context(),
49
+ this.mindset.skills(),
50
+ this.mindset.limits(),
51
+ ]);
52
+ await this.imageDescriber.describeMessageImages(message, models, { context, skills, limits });
53
+ }
35
54
  async processLoop(callback, invalidArgsCount) {
36
55
  const prevItems = await this.memory.findLastItems(16);
37
56
  if (prevItems.length === 0) {
@@ -44,10 +63,13 @@ let ChatBot = class ChatBot {
44
63
  const systemPrompt = await this.mindset.systemPrompt();
45
64
  const tools = this.mindset.tools();
46
65
  const identity = await this.mindset.identity();
47
- const needsVision = prevItems.some((item) => {
48
- const data = item.getData();
49
- return data.type === 'humanMessage' && (data.humanMessage.images?.length ?? 0) > 0;
50
- });
66
+ const prevItemsData = prevItems.map((x) => x.getData());
67
+ // Only media from the pending exchange is actually sent to the model; images
68
+ // in already-answered messages are not, so they must not force a vision model.
69
+ const mediaStart = pendingMediaStartIndex(prevItemsData);
70
+ const needsVision = prevItemsData.some((data, index) => index >= mediaStart &&
71
+ data.type === 'humanMessage' &&
72
+ (data.humanMessage.images?.length ?? 0) > 0);
51
73
  const kind = needsVision ? 'visionLlm' : 'llm';
52
74
  const candidates = await this.mindset.resolveModels(kind);
53
75
  if (candidates.length === 0) {
@@ -57,7 +79,7 @@ let ChatBot = class ChatBot {
57
79
  models: candidates,
58
80
  systemPrompt,
59
81
  tools,
60
- prevItems: prevItems.map((x) => x.getData()),
82
+ prevItems: prevItemsData,
61
83
  });
62
84
  for (const newItemData of newItemsData) {
63
85
  if (newItemData.type === 'functionCall') {
@@ -92,7 +114,8 @@ ChatBot = __decorate([
92
114
  injectable(),
93
115
  __metadata("design:paramtypes", [ChatMemory,
94
116
  ChatAdapter,
95
- MindsetOperator])
117
+ MindsetOperator,
118
+ ImageDescriber])
96
119
  ], ChatBot);
97
120
 
98
121
  export { ChatBot };
@@ -0,0 +1,95 @@
1
+ import { __decorate, __metadata } from 'tslib';
2
+ import { injectable } from '../../core/injection/index.js';
3
+ import { Logger } from '../../core/logger/Logger.js';
4
+ import { ChatAdapter } from './ChatAdapter.js';
5
+
6
+ const DESCRIBE_INSTRUCTIONS = `
7
+ You describe images so an assistant can reference them later in a conversation
8
+ without seeing the image again.
9
+
10
+ Write a single, factual, self-contained paragraph covering the visible content:
11
+ objects, people, scene, layout, colors, and any readable text. Be thorough but
12
+ concise. Do not add opinions, greetings, or markdown — output only the
13
+ description.
14
+ `.trim();
15
+ function buildSystemPrompt(describeContext) {
16
+ const sections = [DESCRIBE_INSTRUCTIONS];
17
+ const mindsetSections = [
18
+ ['Assistant context', describeContext.context],
19
+ ['Assistant skills', describeContext.skills],
20
+ ['Assistant limits', describeContext.limits],
21
+ ].filter(([, value]) => value && value.trim());
22
+ if (mindsetSections.length > 0) {
23
+ sections.push('The assistant you describe images for is outlined below. Use it only to ' +
24
+ 'decide which details deserve emphasis; still describe anything else that ' +
25
+ 'is clearly visible.');
26
+ for (const [title, value] of mindsetSections) {
27
+ sections.push(`## ${title}\n${value.trim()}`);
28
+ }
29
+ }
30
+ return sections.join('\n\n');
31
+ }
32
+ /**
33
+ * Generates a text description for images so the conversation can recall their
34
+ * content on later turns without re-sending (and re-analyzing) the binary.
35
+ *
36
+ * The description is produced once, on arrival, with a single vision-model call
37
+ * and stored on the image itself; from then on it travels with the message text
38
+ * via {@link extractChatMessageText}.
39
+ */
40
+ let ImageDescriber = class ImageDescriber {
41
+ adapter;
42
+ logger = new Logger('wabot:image-describer');
43
+ constructor(adapter) {
44
+ this.adapter = adapter;
45
+ }
46
+ /**
47
+ * Fills `description` for every image on the message that does not have one
48
+ * yet, mutating the images in place. Safe to call with no images or no models:
49
+ * it simply does nothing. Failures are logged and leave the image undescribed
50
+ * rather than aborting the message.
51
+ */
52
+ async describeMessageImages(message, models, describeContext = {}) {
53
+ const pending = message.images?.filter((image) => !image.description);
54
+ if (!pending || pending.length === 0 || models.length === 0)
55
+ return;
56
+ await Promise.all(pending.map(async (image) => {
57
+ const description = await this.describe(image, models, describeContext);
58
+ if (description)
59
+ image.description = description;
60
+ }));
61
+ }
62
+ async describe(image, models, describeContext = {}) {
63
+ if (models.length === 0)
64
+ return undefined;
65
+ try {
66
+ const { nextItems } = await this.adapter.nextItems({
67
+ models,
68
+ systemPrompt: buildSystemPrompt(describeContext),
69
+ tools: [],
70
+ prevItems: [
71
+ {
72
+ type: 'humanMessage',
73
+ humanMessage: { text: 'Describe this image.', images: [image] },
74
+ },
75
+ ],
76
+ });
77
+ const parts = [];
78
+ for (const item of nextItems) {
79
+ if (item.type === 'botMessage' && item.botMessage.text)
80
+ parts.push(item.botMessage.text);
81
+ }
82
+ return parts.join('\n').trim() || undefined;
83
+ }
84
+ catch (err) {
85
+ this.logger.warn(`failed to describe image '${image.id}'`, err instanceof Error ? { message: err.message } : { err });
86
+ return undefined;
87
+ }
88
+ }
89
+ };
90
+ ImageDescriber = __decorate([
91
+ injectable(),
92
+ __metadata("design:paramtypes", [ChatAdapter])
93
+ ], ImageDescriber);
94
+
95
+ export { ImageDescriber };
@@ -9,6 +9,7 @@ function extractChatMessageText(message, options = {}) {
9
9
  id: x.id,
10
10
  name: x.name,
11
11
  mimeType: x.mimeType,
12
+ description: x.description,
12
13
  notAnalyzed: isAnalyzed(x.mimeType, options.supportedImageMimeTypes) ? undefined : true,
13
14
  })),
14
15
  documents: message.documents?.map((x) => ({
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Index of the first chat item that belongs to the current, not-yet-answered
3
+ * exchange — i.e. the item right after the last bot message.
4
+ *
5
+ * Image and document binaries should only be sent to the model for human
6
+ * messages at or after this index. Media in earlier human messages has already
7
+ * been answered by the bot, so its analysis is captured in the bot's replies;
8
+ * re-sending the binary would make the model analyze the same files again on
9
+ * every turn (and re-upload them, wasting tokens).
10
+ *
11
+ * Returns 0 when the bot has not replied yet, so the whole pending exchange
12
+ * keeps its media.
13
+ */
14
+ function pendingMediaStartIndex(items) {
15
+ for (let i = items.length - 1; i >= 0; i--) {
16
+ if (items[i].type === 'botMessage')
17
+ return i + 1;
18
+ }
19
+ return 0;
20
+ }
21
+
22
+ export { pendingMediaStartIndex };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Index of the first item whose media (images/documents) the model has not been
3
+ * shown yet — i.e. right after the last model output, be it a bot message or a
4
+ * function call.
5
+ *
6
+ * A file's binary should only be sent on the model's first exposure to it. Once
7
+ * the model has produced anything in response to a human message (a function
8
+ * call during a tool loop, or a reply), it has already analyzed that message's
9
+ * files; re-sending the bytes on the next call would analyze the same file
10
+ * again. This is the within-turn counterpart of {@link pendingMediaStartIndex}:
11
+ * that one keeps a turn's media "active" for vision-model selection across the
12
+ * whole tool loop, while this one stops re-uploading the bytes after the first
13
+ * call.
14
+ *
15
+ * Returns 0 when the model has not produced any output yet, so a brand-new
16
+ * message keeps its media.
17
+ */
18
+ function unconsumedMediaStartIndex(items) {
19
+ for (let i = items.length - 1; i >= 0; i--) {
20
+ const type = items[i].type;
21
+ if (type === 'botMessage' || type === 'functionCall')
22
+ return i + 1;
23
+ }
24
+ return 0;
25
+ }
26
+
27
+ export { unconsumedMediaStartIndex };
@@ -5,6 +5,7 @@ import '../chat-bot/ChatBot.js';
5
5
  import '../chat-bot/ChatOperator.js';
6
6
  import { ChatRepository } from '../chat-bot/ChatRepository.js';
7
7
  import '../chat-bot/UnionChatAdapter.js';
8
+ import '../chat-bot/ImageDescriber.js';
8
9
  import { injectable } from '../../core/injection/index.js';
9
10
  import '../chat-bot/metadata/ChatAdapterMetadataStore.js';
10
11
  import 'uuid';
@@ -8,6 +8,7 @@ import { ChatMemory } from '../chat-bot/ChatMemory.js';
8
8
  import '../chat-bot/ChatOperator.js';
9
9
  import { ChatRepository } from '../chat-bot/ChatRepository.js';
10
10
  import '../chat-bot/UnionChatAdapter.js';
11
+ import '../chat-bot/ImageDescriber.js';
11
12
  import '../chat-bot/metadata/ChatAdapterMetadataStore.js';
12
13
  import 'uuid';
13
14
  import { ChatBotMetadataStore } from '../chat-bot/metadata/ChatBotMetadataStore.js';
@@ -7,6 +7,7 @@ import '../chat-bot/ChatBot.js';
7
7
  import '../chat-bot/ChatOperator.js';
8
8
  import { ChatRepository } from '../chat-bot/ChatRepository.js';
9
9
  import '../chat-bot/UnionChatAdapter.js';
10
+ import '../chat-bot/ImageDescriber.js';
10
11
  import '../chat-bot/metadata/ChatAdapterMetadataStore.js';
11
12
  import 'uuid';
12
13
  import '../chat-bot/metadata/ChatBotMetadataStore.js';
@@ -953,6 +953,8 @@ interface IChatMessagesPublicFile {
953
953
  base64Url?: undefined;
954
954
  mimeType: string;
955
955
  id: string;
956
+ /** Text description of the file content, used to recall it without re-analyzing the binary. */
957
+ description?: string;
956
958
  }
957
959
  interface IChatMessagesPrivateFile {
958
960
  name?: string;
@@ -960,6 +962,8 @@ interface IChatMessagesPrivateFile {
960
962
  base64Url: string;
961
963
  mimeType: string;
962
964
  id: string;
965
+ /** Text description of the file content, used to recall it without re-analyzing the binary. */
966
+ description?: string;
963
967
  }
964
968
  type IChatMessageFile = IChatMessagesPrivateFile | IChatMessagesPublicFile;
965
969
 
@@ -1067,13 +1071,43 @@ interface IChatBot {
1067
1071
  sendMessage(message: IChatMessage, callback: (message: IChatMessage) => Promise<void>): void | Promise<void>;
1068
1072
  }
1069
1073
 
1074
+ /** Slice of the mindset used to focus the description on what the assistant cares about. */
1075
+ interface IImageDescribeContext {
1076
+ context?: string;
1077
+ skills?: string;
1078
+ limits?: string;
1079
+ }
1080
+ /**
1081
+ * Generates a text description for images so the conversation can recall their
1082
+ * content on later turns without re-sending (and re-analyzing) the binary.
1083
+ *
1084
+ * The description is produced once, on arrival, with a single vision-model call
1085
+ * and stored on the image itself; from then on it travels with the message text
1086
+ * via {@link extractChatMessageText}.
1087
+ */
1088
+ declare class ImageDescriber {
1089
+ private adapter;
1090
+ private logger;
1091
+ constructor(adapter: ChatAdapter);
1092
+ /**
1093
+ * Fills `description` for every image on the message that does not have one
1094
+ * yet, mutating the images in place. Safe to call with no images or no models:
1095
+ * it simply does nothing. Failures are logged and leave the image undescribed
1096
+ * rather than aborting the message.
1097
+ */
1098
+ describeMessageImages(message: IChatMessage, models: IMindsetModelRef[], describeContext?: IImageDescribeContext): Promise<void>;
1099
+ describe(image: IChatMessageImage, models: IMindsetModelRef[], describeContext?: IImageDescribeContext): Promise<string | undefined>;
1100
+ }
1101
+
1070
1102
  declare class ChatBot implements IChatBot {
1071
1103
  private memory;
1072
1104
  private adapter;
1073
1105
  private mindset;
1106
+ private imageDescriber;
1074
1107
  private logger;
1075
- constructor(memory: ChatMemory, adapter: ChatAdapter, mindset: MindsetOperator);
1108
+ constructor(memory: ChatMemory, adapter: ChatAdapter, mindset: MindsetOperator, imageDescriber: ImageDescriber);
1076
1109
  sendMessage(message: IChatMessage, callback: (message: IChatMessage) => Promise<void>): Promise<void>;
1110
+ private describeImages;
1077
1111
  protected processLoop(callback: (message: IChatMessage) => Promise<void>, invalidArgsCount: number): Promise<void>;
1078
1112
  }
1079
1113
 
@@ -1163,10 +1197,44 @@ declare class ChatBotMetadataStore {
1163
1197
  getChatBotsMetadata(): IChatBotMetadata[];
1164
1198
  }
1165
1199
 
1200
+ /**
1201
+ * Index of the first chat item that belongs to the current, not-yet-answered
1202
+ * exchange — i.e. the item right after the last bot message.
1203
+ *
1204
+ * Image and document binaries should only be sent to the model for human
1205
+ * messages at or after this index. Media in earlier human messages has already
1206
+ * been answered by the bot, so its analysis is captured in the bot's replies;
1207
+ * re-sending the binary would make the model analyze the same files again on
1208
+ * every turn (and re-upload them, wasting tokens).
1209
+ *
1210
+ * Returns 0 when the bot has not replied yet, so the whole pending exchange
1211
+ * keeps its media.
1212
+ */
1213
+ declare function pendingMediaStartIndex(items: IChatItem[]): number;
1214
+
1166
1215
  declare function runChatAdapters(adapters: IConstructor<IChatAdapter>[]): void;
1167
1216
 
1168
1217
  declare function safeJsonParse<T = unknown>(json: string | undefined | null, context?: string): T;
1169
1218
 
1219
+ /**
1220
+ * Index of the first item whose media (images/documents) the model has not been
1221
+ * shown yet — i.e. right after the last model output, be it a bot message or a
1222
+ * function call.
1223
+ *
1224
+ * A file's binary should only be sent on the model's first exposure to it. Once
1225
+ * the model has produced anything in response to a human message (a function
1226
+ * call during a tool loop, or a reply), it has already analyzed that message's
1227
+ * files; re-sending the bytes on the next call would analyze the same file
1228
+ * again. This is the within-turn counterpart of {@link pendingMediaStartIndex}:
1229
+ * that one keeps a turn's media "active" for vision-model selection across the
1230
+ * whole tool loop, while this one stops re-uploading the bytes after the first
1231
+ * call.
1232
+ *
1233
+ * Returns 0 when the model has not produced any output yet, so a brand-new
1234
+ * message keeps its media.
1235
+ */
1236
+ declare function unconsumedMediaStartIndex(items: IChatItem[]): number;
1237
+
1170
1238
  interface IProjectRunnerConfig {
1171
1239
  directories?: string[];
1172
1240
  exclude?: string[];
@@ -2056,8 +2124,51 @@ declare class CmdChannelConfig {
2056
2124
  constructor(route: string);
2057
2125
  }
2058
2126
 
2127
+ interface ICmdChannelEntry {
2128
+ route: string;
2129
+ }
2130
+ interface ICmdImage {
2131
+ name?: string;
2132
+ mimeType: string;
2133
+ /** Data URL: `data:<mimeType>;base64,<...>`. */
2134
+ base64Url: string;
2135
+ }
2136
+ type CmdClientMessage = {
2137
+ type: 'hello';
2138
+ } | {
2139
+ type: 'select';
2140
+ route: string;
2141
+ } | {
2142
+ type: 'message';
2143
+ text?: string;
2144
+ images?: ICmdImage[];
2145
+ } | {
2146
+ type: 'clear';
2147
+ };
2148
+ type CmdServerMessage = {
2149
+ type: 'channels';
2150
+ list: ICmdChannelEntry[];
2151
+ } | {
2152
+ type: 'selected';
2153
+ route: string;
2154
+ } | {
2155
+ type: 'reply';
2156
+ senderName?: string;
2157
+ text: string;
2158
+ } | {
2159
+ type: 'cleared';
2160
+ route: string;
2161
+ } | {
2162
+ type: 'error';
2163
+ message: string;
2164
+ };
2165
+
2166
+ interface ICmdIncomingMessage {
2167
+ text?: string;
2168
+ images?: ICmdImage[];
2169
+ }
2059
2170
  interface ICmdChannelHandlers {
2060
- onMessage: (text: string, reply: (response: {
2171
+ onMessage: (message: ICmdIncomingMessage, reply: (response: {
2061
2172
  senderName?: string;
2062
2173
  text: string;
2063
2174
  }) => void) => Promise<void> | void;
@@ -2113,38 +2224,6 @@ declare function readJsonFromFile<T>(filename: string): T | null;
2113
2224
 
2114
2225
  declare function cmdChannelSocketPath(): string;
2115
2226
 
2116
- interface ICmdChannelEntry {
2117
- route: string;
2118
- }
2119
- type CmdClientMessage = {
2120
- type: 'hello';
2121
- } | {
2122
- type: 'select';
2123
- route: string;
2124
- } | {
2125
- type: 'message';
2126
- text: string;
2127
- } | {
2128
- type: 'clear';
2129
- };
2130
- type CmdServerMessage = {
2131
- type: 'channels';
2132
- list: ICmdChannelEntry[];
2133
- } | {
2134
- type: 'selected';
2135
- route: string;
2136
- } | {
2137
- type: 'reply';
2138
- senderName?: string;
2139
- text: string;
2140
- } | {
2141
- type: 'cleared';
2142
- route: string;
2143
- } | {
2144
- type: 'error';
2145
- message: string;
2146
- };
2147
-
2148
2227
  declare function runCmdClient(): void;
2149
2228
 
2150
2229
  interface ISocketChannelConfig {
@@ -2234,8 +2313,11 @@ declare class TelegramChannel implements IChatChannel {
2234
2313
  private config;
2235
2314
  static channelName: "TelegramChannel";
2236
2315
  private bot;
2316
+ private logger;
2237
2317
  constructor(config: TelegramChannelConfig);
2238
2318
  listen(callback: (message: ITelegramChannelMessage) => Promise<void>): void;
2319
+ private extractMedia;
2320
+ private downloadChatFile;
2239
2321
  connect(): void;
2240
2322
  disconnect(): void;
2241
2323
  }
@@ -2683,4 +2765,4 @@ declare function HtmlModule(options: IHtmlModuleOptions): {
2683
2765
  new (): {};
2684
2766
  };
2685
2767
 
2686
- export { AnthropicChatAdapter, ApiKey, ApiKeyGuardMiddleware, ApiKeyHandshakeGuardMiddleware, ApiKeyRepository, Async, AsyncMetadataStore, Auth, Chat, ChatAdapter, ChatAdapterMetadataStore, ChatAdapterRegistry, ChatBot, ChatBotMetadataStore, ChatItem, ChatMemory, ChatOperator, ChatRepository, ChatResolver, type ClientMap, CmdChannel, CmdChannelConfig, CmdChannelServer, type CmdClientMessage, type CmdServerMessage, type ConfigReference, type ConfigReferenceType, ConfigResolver, Container, ControllerMetadataStore, CronJob, CronJobRepository, CrudRepository, CustomError, DeepSeekChatAdapter, DescriptionMetadataStore, EXPRESS_REQ, EXPRESS_RES, Entity, Env, type ErrorSeverity, ExpressProvider, GoogleChatAdapter, type GoogleChatAdapterV2Options, HtmlModule, HttpServerProvider, type IApiKeyData, type IApiKeyRepository, type IArrayValidationError, type IArrayValidationResult, type IBotMessageItem, type IBuiltQuery, type IChannelMessage, type IChannelMetadata, type IChatAdapter, type IChatAdapterDecoratorConfig, type IChatAdapterMetadata, type IChatAdapterNextItemsReq, type IChatAdapterNextItemsRes, type IChatAssociation, type IChatBot, type IChatBotMetadata, type IChatChannel, type IChatConnection, type IChatControllerMetadata, type IChatData, type IChatItem, type IChatItemData, type IChatItemType, type IChatMemory, type IChatMessage, type IChatMessageDocument, type IChatMessageFile, type IChatMessageImage, type IChatMessagesPrivateFile, type IChatMessagesPublicFile, type IChatRepository, type IChatType, type ICmdChannelEntry, type ICmdChannelHandlers, type ICmdChannelMessage, type ICmdReceivedMessage, type ICommandConfig, type ICommandHandler, type ICommandHandlerConfig, type IConstructor, type ICronConfig, type ICronHandler, type ICronJobData, type ICronJobRepository, type ICrudRepository, type ICustomErrorData, type IDedupConfig, type IDescriptionMetadata, type IEndPointConfig, type IEndPointMetadata, type IEntityData, type IEnvType, type IErrorHandlersConfig, type IErrorMonitor, type IErrorMonitorContext, type IExtractChatMessageTextOptions, type IFunctionCall, type IFunctionCallItem, type IGenerateApiKeyReq, type IGenerateApiKeyRes, type IHandshakeMiddleware, type IHandshakeMiddlewareMetadata, type IHtmlModuleOptions, type IHumanMessageItem, type IJobData, type IJobOptions, type IJobRepository, type IJwtRefreshTokenData, type IJwtRefreshTokenRepository, type IKapsoChannelConfig, type IKapsoChannelMessage, type IKapsoChannelMessageListener, type IKapsoChatMessage, type IKapsoConversation, type IKapsoEvent, type IKapsoIncomingMessage, type IKapsoMessageReceivedEvent, type IKapsoReceivedMessage, type IKapsoUnknownEvent, type ILanguageModelUsage, type ILockKey, type ILocker, type ILockerKey, type IMemoryRepositoryAdapterOptions, type IMessageContext, type IMiddleware, type IMiddlewareMetadata, type IMindset, type IMindsetConfig, type IMindsetIdentity, type IMindsetLlm, type IMindsetMetadata, type IMindsetModelKind, type IMindsetModelRef, type IMindsetModels, type IMindsetModuleConfig, type IMindsetModuleMetadata, type IMindsetParameterSchema, type IMindsetTool, type IMindsetToolParameter, type IModelValidationError, type IModelValidationResult, type IModelValidatorsInfo, type IMoneyData, type IPersistentData, type IPgRepositoryConfig, type IProjectRunnerConfig, type IPropertyValidatorInfo, type IQueryAst, type IQueryCondition, type IQueryMethodMetadata, type IQueryOrderBy, type IReceivedMessage, type IRemoteApiKeyFetcher, type IRepositoryAdapter, type IRepositoryConfig, type IRepositoryRuntime, type IRestControllerConfig, type IRestControllerMetadata, type IScanProjectFilesOptions, type IScheduleAt, type IScheduleDelay, type ISendWhatsAppMessageReq, type ISendWhatsAppTemplateReq, type ISocketChannelConfig, type ISocketChannelMessage, type ISocketChannelReceivedMessage, type ISocketControllerConfig, type ISocketControllerMetadata, type ISocketEventConfig, type ISocketEventMetadata, type ISocketReceivedMessage, type IStorableData, type ITelegramChannelConfig, type ITelegramChannelMessage, type ITelegramReceivedMessage, type ITransactionAdapter, type IValidateArrayOptions, type IValidateArrayOptionsWithItemsValidators, type IValidateInputShape, type IValidateIsInOptions, type IValidateIsRecordOptions, type IValidateMaxOptions, type IValidateMinOptions, type IValidationError, type IValidationResult, type IValidator, type IValidatorMetadata, type IWasenderChannelConfig, type IWasenderChannelMessageListener, type IWasenderDeviceListMetadata, type IWasenderEvent, type IWasenderMessageContent, type IWasenderMessageContextInfo, type IWasenderMessageKey, type IWasenderMessageReceivedData, type IWasenderMessageReceivedEvent, type IWasenderQrUpdatedEvent, type IWasenderReceivedMessage, type IWhatsAppCloudContact, type IWhatsAppCloudMessage, type IWhatsAppCloudMessageMetadata, type IWhatsAppCloudTemplate, type IWhatsAppCloudTemplateComponent, type IWhatsAppCloudTemplateResponse, type IWhatsAppCloudWebhookPayload, type IWhatsAppSender, type IWhatsAppTemplateData, type IWhatsAppTemplateParameter, type IchatControllerConfig, InMemoryChatMemory, InMemoryChatRepository, InMemoryCronJobRepository, InMemoryJobRepository, InMemoryLockKey, InMemoryLocker, Job, JobRepository, JobRunner, Jwt, JwtAccessAndRefreshTokenDto, JwtConfig, JwtGuardMiddleware, JwtHandshakeGuardMiddleware, JwtRefreshToken, JwtRefreshTokenRepository, JwtSigner, JwtTokenDto, KapsoChannel, KapsoChannelConfig, KapsoReceiver, KapsoSender, KapsoWebhookController, Lifecycle, Locker, Logger, MEMORY_ADAPTER_ID, Mapper, MemoryRepositoryAdapter, MemoryRepositoryExtension, Mindset, MindsetMetadataStore, MindsetOperator, Money, MoneyDto, OpenRouterChatAdapter, OpenaiChatAdapter, PG_ADAPTER_ID, Password, type PasswordHashOptions, Persistent, PgApiKeyRepository, PgChatMemory, PgChatRepository, PgCronJobRepository, PgCrudRepository, PgJobRepository, PgJsonRepositoryAdapter, PgJwtRefreshTokenRepository, PgLockKey, PgLocker, PgRepositoryBase, PgRepositoryBase as PgRepositoryExtension, PgTransactionAdapter, ProjectRunner, type QueryConnector, type QueryOperator, type QueryPrefix, Random, RemoteApiKeyRepository, RepositoryAdapterRegistry, RepositoryMetadataStore, type ResolvedConfig, RestControllerMetadataStore, RestRequest, SocketChannel, SocketChannelConfig, SocketChannelMessageFile, SocketChannelReceivedMessage, SocketControllerMetadataStore, SocketServerConfig, SocketServerProvider, Storable, TelegramChannel, TelegramChannelConfig, TransactionMetadataStore, UnionChatAdapter, ValidationMetadataStore, WabotChatAdapter, WasenderChannel, WasenderChannelConfig, WasenderReceiver, WasenderSender, WasenderWebhookController, WhatsAppApiSender, WhatsAppReceiverByCloudApi, WhatsAppSender, apiKeyGuard, apiKeyHandshakeGuard, bool, boolArr, buildQuerySql, chatAdapter, chatBot, chatController, chatItemTypeOptions, cmd, cmdChannelName, cmdChannelSocketPath, command, commandHandler, computeDedupKey, container, cronHandler, description, errorToPlainObject, evaluateQueryAst, extractChatMessageText, extractNumberFromWasenderMessageKey, getClientMap, getPgClient, handshakeMiddlewares, inject, injectable, isArray, isBoolean, isChatMessageEmpty, isDate, isIn, isModel, isNotEmpty, isNumber, isOptional, isPresent, isRecord, isRetryableError, isString, jwtGuard, jwtHandshakeGuard, kapso, kapsoChannelName, markdownToTelegramHtml, max, memExtension, middleware, min, mindset, mindsetModule, modelInfo, num, numArr, obj, onDelete, onGet, onPost, onPut, onSocketEvent, parseQueryMethodName, pgExtension, pgStorage, query, queryExtension, readJsonFromFile, repository, resolveConfigReferences, restController, run, runChatAdapters, runChatControllers, runCmdClient, runCommandHandlers, runCronHandlers, runRestControllers, runSocketControllers, safeJsonParse, scanProjectFiles, scoped, setupErrorHandlers, singleton, socket, socketChannelName, socketController, stopCommandHandlers, stopCronHandlers, str, strArr, telegram, telegramChannelName, transaction, validateAndTransform, validateArray, validateIsBoolean, validateIsDate, validateIsIn, validateIsNotEmpty, validateIsNumber, validateIsPresent, validateIsRecord, validateIsString, validateMax, validateMin, validateModel, wasender, wasenderChannelName, withPgClient, withPgTransaction, writeJsonToFile };
2768
+ export { AnthropicChatAdapter, ApiKey, ApiKeyGuardMiddleware, ApiKeyHandshakeGuardMiddleware, ApiKeyRepository, Async, AsyncMetadataStore, Auth, Chat, ChatAdapter, ChatAdapterMetadataStore, ChatAdapterRegistry, ChatBot, ChatBotMetadataStore, ChatItem, ChatMemory, ChatOperator, ChatRepository, ChatResolver, type ClientMap, CmdChannel, CmdChannelConfig, CmdChannelServer, type CmdClientMessage, type CmdServerMessage, type ConfigReference, type ConfigReferenceType, ConfigResolver, Container, ControllerMetadataStore, CronJob, CronJobRepository, CrudRepository, CustomError, DeepSeekChatAdapter, DescriptionMetadataStore, EXPRESS_REQ, EXPRESS_RES, Entity, Env, type ErrorSeverity, ExpressProvider, GoogleChatAdapter, type GoogleChatAdapterV2Options, HtmlModule, HttpServerProvider, type IApiKeyData, type IApiKeyRepository, type IArrayValidationError, type IArrayValidationResult, type IBotMessageItem, type IBuiltQuery, type IChannelMessage, type IChannelMetadata, type IChatAdapter, type IChatAdapterDecoratorConfig, type IChatAdapterMetadata, type IChatAdapterNextItemsReq, type IChatAdapterNextItemsRes, type IChatAssociation, type IChatBot, type IChatBotMetadata, type IChatChannel, type IChatConnection, type IChatControllerMetadata, type IChatData, type IChatItem, type IChatItemData, type IChatItemType, type IChatMemory, type IChatMessage, type IChatMessageDocument, type IChatMessageFile, type IChatMessageImage, type IChatMessagesPrivateFile, type IChatMessagesPublicFile, type IChatRepository, type IChatType, type ICmdChannelEntry, type ICmdChannelHandlers, type ICmdChannelMessage, type ICmdImage, type ICmdIncomingMessage, type ICmdReceivedMessage, type ICommandConfig, type ICommandHandler, type ICommandHandlerConfig, type IConstructor, type ICronConfig, type ICronHandler, type ICronJobData, type ICronJobRepository, type ICrudRepository, type ICustomErrorData, type IDedupConfig, type IDescriptionMetadata, type IEndPointConfig, type IEndPointMetadata, type IEntityData, type IEnvType, type IErrorHandlersConfig, type IErrorMonitor, type IErrorMonitorContext, type IExtractChatMessageTextOptions, type IFunctionCall, type IFunctionCallItem, type IGenerateApiKeyReq, type IGenerateApiKeyRes, type IHandshakeMiddleware, type IHandshakeMiddlewareMetadata, type IHtmlModuleOptions, type IHumanMessageItem, type IImageDescribeContext, type IJobData, type IJobOptions, type IJobRepository, type IJwtRefreshTokenData, type IJwtRefreshTokenRepository, type IKapsoChannelConfig, type IKapsoChannelMessage, type IKapsoChannelMessageListener, type IKapsoChatMessage, type IKapsoConversation, type IKapsoEvent, type IKapsoIncomingMessage, type IKapsoMessageReceivedEvent, type IKapsoReceivedMessage, type IKapsoUnknownEvent, type ILanguageModelUsage, type ILockKey, type ILocker, type ILockerKey, type IMemoryRepositoryAdapterOptions, type IMessageContext, type IMiddleware, type IMiddlewareMetadata, type IMindset, type IMindsetConfig, type IMindsetIdentity, type IMindsetLlm, type IMindsetMetadata, type IMindsetModelKind, type IMindsetModelRef, type IMindsetModels, type IMindsetModuleConfig, type IMindsetModuleMetadata, type IMindsetParameterSchema, type IMindsetTool, type IMindsetToolParameter, type IModelValidationError, type IModelValidationResult, type IModelValidatorsInfo, type IMoneyData, type IPersistentData, type IPgRepositoryConfig, type IProjectRunnerConfig, type IPropertyValidatorInfo, type IQueryAst, type IQueryCondition, type IQueryMethodMetadata, type IQueryOrderBy, type IReceivedMessage, type IRemoteApiKeyFetcher, type IRepositoryAdapter, type IRepositoryConfig, type IRepositoryRuntime, type IRestControllerConfig, type IRestControllerMetadata, type IScanProjectFilesOptions, type IScheduleAt, type IScheduleDelay, type ISendWhatsAppMessageReq, type ISendWhatsAppTemplateReq, type ISocketChannelConfig, type ISocketChannelMessage, type ISocketChannelReceivedMessage, type ISocketControllerConfig, type ISocketControllerMetadata, type ISocketEventConfig, type ISocketEventMetadata, type ISocketReceivedMessage, type IStorableData, type ITelegramChannelConfig, type ITelegramChannelMessage, type ITelegramReceivedMessage, type ITransactionAdapter, type IValidateArrayOptions, type IValidateArrayOptionsWithItemsValidators, type IValidateInputShape, type IValidateIsInOptions, type IValidateIsRecordOptions, type IValidateMaxOptions, type IValidateMinOptions, type IValidationError, type IValidationResult, type IValidator, type IValidatorMetadata, type IWasenderChannelConfig, type IWasenderChannelMessageListener, type IWasenderDeviceListMetadata, type IWasenderEvent, type IWasenderMessageContent, type IWasenderMessageContextInfo, type IWasenderMessageKey, type IWasenderMessageReceivedData, type IWasenderMessageReceivedEvent, type IWasenderQrUpdatedEvent, type IWasenderReceivedMessage, type IWhatsAppCloudContact, type IWhatsAppCloudMessage, type IWhatsAppCloudMessageMetadata, type IWhatsAppCloudTemplate, type IWhatsAppCloudTemplateComponent, type IWhatsAppCloudTemplateResponse, type IWhatsAppCloudWebhookPayload, type IWhatsAppSender, type IWhatsAppTemplateData, type IWhatsAppTemplateParameter, type IchatControllerConfig, ImageDescriber, InMemoryChatMemory, InMemoryChatRepository, InMemoryCronJobRepository, InMemoryJobRepository, InMemoryLockKey, InMemoryLocker, Job, JobRepository, JobRunner, Jwt, JwtAccessAndRefreshTokenDto, JwtConfig, JwtGuardMiddleware, JwtHandshakeGuardMiddleware, JwtRefreshToken, JwtRefreshTokenRepository, JwtSigner, JwtTokenDto, KapsoChannel, KapsoChannelConfig, KapsoReceiver, KapsoSender, KapsoWebhookController, Lifecycle, Locker, Logger, MEMORY_ADAPTER_ID, Mapper, MemoryRepositoryAdapter, MemoryRepositoryExtension, Mindset, MindsetMetadataStore, MindsetOperator, Money, MoneyDto, OpenRouterChatAdapter, OpenaiChatAdapter, PG_ADAPTER_ID, Password, type PasswordHashOptions, Persistent, PgApiKeyRepository, PgChatMemory, PgChatRepository, PgCronJobRepository, PgCrudRepository, PgJobRepository, PgJsonRepositoryAdapter, PgJwtRefreshTokenRepository, PgLockKey, PgLocker, PgRepositoryBase, PgRepositoryBase as PgRepositoryExtension, PgTransactionAdapter, ProjectRunner, type QueryConnector, type QueryOperator, type QueryPrefix, Random, RemoteApiKeyRepository, RepositoryAdapterRegistry, RepositoryMetadataStore, type ResolvedConfig, RestControllerMetadataStore, RestRequest, SocketChannel, SocketChannelConfig, SocketChannelMessageFile, SocketChannelReceivedMessage, SocketControllerMetadataStore, SocketServerConfig, SocketServerProvider, Storable, TelegramChannel, TelegramChannelConfig, TransactionMetadataStore, UnionChatAdapter, ValidationMetadataStore, WabotChatAdapter, WasenderChannel, WasenderChannelConfig, WasenderReceiver, WasenderSender, WasenderWebhookController, WhatsAppApiSender, WhatsAppReceiverByCloudApi, WhatsAppSender, apiKeyGuard, apiKeyHandshakeGuard, bool, boolArr, buildQuerySql, chatAdapter, chatBot, chatController, chatItemTypeOptions, cmd, cmdChannelName, cmdChannelSocketPath, command, commandHandler, computeDedupKey, container, cronHandler, description, errorToPlainObject, evaluateQueryAst, extractChatMessageText, extractNumberFromWasenderMessageKey, getClientMap, getPgClient, handshakeMiddlewares, inject, injectable, isArray, isBoolean, isChatMessageEmpty, isDate, isIn, isModel, isNotEmpty, isNumber, isOptional, isPresent, isRecord, isRetryableError, isString, jwtGuard, jwtHandshakeGuard, kapso, kapsoChannelName, markdownToTelegramHtml, max, memExtension, middleware, min, mindset, mindsetModule, modelInfo, num, numArr, obj, onDelete, onGet, onPost, onPut, onSocketEvent, parseQueryMethodName, pendingMediaStartIndex, pgExtension, pgStorage, query, queryExtension, readJsonFromFile, repository, resolveConfigReferences, restController, run, runChatAdapters, runChatControllers, runCmdClient, runCommandHandlers, runCronHandlers, runRestControllers, runSocketControllers, safeJsonParse, scanProjectFiles, scoped, setupErrorHandlers, singleton, socket, socketChannelName, socketController, stopCommandHandlers, stopCronHandlers, str, strArr, telegram, telegramChannelName, transaction, unconsumedMediaStartIndex, validateAndTransform, validateArray, validateIsBoolean, validateIsDate, validateIsIn, validateIsNotEmpty, validateIsNumber, validateIsPresent, validateIsRecord, validateIsString, validateMax, validateMin, validateModel, wasender, wasenderChannelName, withPgClient, withPgTransaction, writeJsonToFile };
package/dist/src/index.js CHANGED
@@ -71,14 +71,17 @@ export { ChatRepository } from './feature/chat-bot/ChatRepository.js';
71
71
  export { chatItemTypeOptions } from './feature/chat-bot/IChatItem.js';
72
72
  export { UnionChatAdapter } from './feature/chat-bot/UnionChatAdapter.js';
73
73
  export { extractChatMessageText } from './feature/chat-bot/extractChatMessageText.js';
74
+ export { ImageDescriber } from './feature/chat-bot/ImageDescriber.js';
74
75
  export { isChatMessageEmpty } from './feature/chat-bot/isChatMessageEmpty.js';
75
76
  export { isRetryableError } from './feature/chat-bot/isRetryableError.js';
76
77
  export { chatAdapter } from './feature/chat-bot/metadata/@chatAdapter.js';
77
78
  export { chatBot } from './feature/chat-bot/metadata/@chatBot.js';
78
79
  export { ChatAdapterMetadataStore } from './feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
79
80
  export { ChatBotMetadataStore } from './feature/chat-bot/metadata/ChatBotMetadataStore.js';
81
+ export { pendingMediaStartIndex } from './feature/chat-bot/pendingMediaStartIndex.js';
80
82
  export { runChatAdapters } from './feature/chat-bot/runChatAdapters.js';
81
83
  export { safeJsonParse } from './feature/chat-bot/safeJsonParse.js';
84
+ export { unconsumedMediaStartIndex } from './feature/chat-bot/unconsumedMediaStartIndex.js';
82
85
  export { chatController } from './feature/chat-controller/metadata/controller/@chatController.js';
83
86
  export { ControllerMetadataStore } from './feature/chat-controller/metadata/ControllerMetadataStore.js';
84
87
  export { ChatResolver } from './feature/chat-controller/ChatResolver.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wabot-dev/framework",
3
- "version": "0.9.22",
3
+ "version": "0.9.24",
4
4
  "description": "Framework for IA Chat Bots",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -52,7 +52,7 @@
52
52
  "types:check": "tsc --noEmit",
53
53
  "elia:dev": "node --import @yucacodes/ts --import ./env.mjs ./test/elia/_run_.ts",
54
54
  "elia:watch": "node --watch --import @yucacodes/ts --import ./env.mjs ./test/elia/_run_.ts",
55
- "elia:cmd:channel": "node --import @yucacodes/ts ./test/elia/_cmd_.ts"
55
+ "elia:cmd": "node --import @yucacodes/ts ./test/elia/_cmd_.ts"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@rollup/plugin-alias": "5.1.1",