@wabot-dev/framework 0.9.23 → 0.9.25

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.
@@ -14,6 +14,7 @@ import 'uuid';
14
14
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
15
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
16
16
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
17
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
17
18
  import { Anthropic } from '@anthropic-ai/sdk';
18
19
 
19
20
  const ANTHROPIC_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -60,10 +61,11 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
60
61
  }
61
62
  mapChatItems(chatItems) {
62
63
  const messages = [];
63
- for (const chatItem of chatItems) {
64
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
65
+ chatItems.forEach((chatItem, index) => {
64
66
  switch (chatItem.type) {
65
67
  case 'humanMessage':
66
- messages.push(this.mapHumanMessage(chatItem.humanMessage));
68
+ messages.push(this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
67
69
  break;
68
70
  case 'botMessage':
69
71
  messages.push(this.mapBotMessage(chatItem.botMessage));
@@ -72,10 +74,10 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
72
74
  messages.push(...this.mapFunctionCall(chatItem.functionCall));
73
75
  break;
74
76
  }
75
- }
77
+ });
76
78
  return messages;
77
79
  }
78
- mapHumanMessage(item) {
80
+ mapHumanMessage(item, includeMedia) {
79
81
  if (isChatMessageEmpty(item)) {
80
82
  throw new Error('User message content is empty');
81
83
  }
@@ -87,14 +89,14 @@ let AnthropicChatAdapter = class AnthropicChatAdapter {
87
89
  supportedDocumentMimeTypes: ANTHROPIC_SUPPORTED_DOCUMENT_MIME_TYPES,
88
90
  }),
89
91
  });
90
- if (item.images) {
92
+ if (includeMedia && item.images) {
91
93
  for (const image of item.images) {
92
94
  if (!ANTHROPIC_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
93
95
  continue;
94
96
  blocks.push({ type: 'image', source: this.toAnthropicImageSource(image) });
95
97
  }
96
98
  }
97
- if (item.documents) {
99
+ if (includeMedia && item.documents) {
98
100
  for (const doc of item.documents) {
99
101
  if (!ANTHROPIC_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
100
102
  continue;
@@ -15,6 +15,7 @@ import 'uuid';
15
15
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
16
16
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
17
17
  import { safeJsonParse } from '../../../feature/chat-bot/safeJsonParse.js';
18
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
18
19
  import { GoogleGenAI } from '@google/genai';
19
20
 
20
21
  const GOOGLE_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -82,10 +83,11 @@ let GoogleChatAdapter = class GoogleChatAdapter {
82
83
  }
83
84
  async mapChatItems(chatItems) {
84
85
  const contents = [];
85
- for (const chatItem of chatItems) {
86
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
87
+ for (const [index, chatItem] of chatItems.entries()) {
86
88
  switch (chatItem.type) {
87
89
  case 'humanMessage':
88
- contents.push(await this.mapHumanMessage(chatItem.humanMessage));
90
+ contents.push(await this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
89
91
  break;
90
92
  case 'botMessage':
91
93
  contents.push(this.mapBotMessage(chatItem.botMessage));
@@ -97,7 +99,7 @@ let GoogleChatAdapter = class GoogleChatAdapter {
97
99
  }
98
100
  return contents;
99
101
  }
100
- async mapHumanMessage(item) {
102
+ async mapHumanMessage(item, includeMedia) {
101
103
  if (isChatMessageEmpty(item)) {
102
104
  throw new Error('User message content is empty');
103
105
  }
@@ -109,14 +111,14 @@ let GoogleChatAdapter = class GoogleChatAdapter {
109
111
  }),
110
112
  });
111
113
  const filesToSend = [];
112
- if (item.images) {
114
+ if (includeMedia && item.images) {
113
115
  for (const image of item.images) {
114
116
  if (!GOOGLE_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
115
117
  continue;
116
118
  filesToSend.push(image);
117
119
  }
118
120
  }
119
- if (item.documents) {
121
+ if (includeMedia && item.documents) {
120
122
  for (const doc of item.documents) {
121
123
  if (!GOOGLE_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
122
124
  continue;
@@ -12,6 +12,7 @@ import 'uuid';
12
12
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
13
13
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
14
14
  import '../../../core/error/setupErrorHandlers.js';
15
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
15
16
  import { Logger } from '../../../core/logger/Logger.js';
16
17
  import { OpenAI } from 'openai';
17
18
 
@@ -51,10 +52,11 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
51
52
  }
52
53
  mapChatItems(chatItems) {
53
54
  const openIaInput = [];
54
- for (const chatItem of chatItems) {
55
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
56
+ chatItems.forEach((chatItem, index) => {
55
57
  switch (chatItem.type) {
56
58
  case 'humanMessage':
57
- openIaInput.push(this.mapConectionMessage(chatItem.humanMessage));
59
+ openIaInput.push(this.mapConectionMessage(chatItem.humanMessage, index >= mediaStart));
58
60
  break;
59
61
  case 'botMessage':
60
62
  openIaInput.push(this.mapBotMessage(chatItem.botMessage));
@@ -63,10 +65,10 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
63
65
  openIaInput.push(...this.mapFunctionCall(chatItem.functionCall));
64
66
  break;
65
67
  }
66
- }
68
+ });
67
69
  return openIaInput;
68
70
  }
69
- mapConectionMessage(item) {
71
+ mapConectionMessage(item, includeMedia) {
70
72
  if (isChatMessageEmpty(item)) {
71
73
  throw new Error('User message content is empty');
72
74
  }
@@ -78,7 +80,7 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
78
80
  supportedDocumentMimeTypes: OPENAI_SUPPORTED_DOCUMENT_MIME_TYPES,
79
81
  }),
80
82
  });
81
- if (item.images) {
83
+ if (includeMedia && item.images) {
82
84
  for (const image of item.images) {
83
85
  if (!OPENAI_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
84
86
  continue;
@@ -89,7 +91,7 @@ let OpenaiChatAdapter = class OpenaiChatAdapter {
89
91
  });
90
92
  }
91
93
  }
92
- if (item.documents) {
94
+ if (includeMedia && item.documents) {
93
95
  for (const doc of item.documents) {
94
96
  if (!OPENAI_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
95
97
  continue;
@@ -14,6 +14,7 @@ import 'uuid';
14
14
  import '../../../feature/chat-bot/metadata/ChatBotMetadataStore.js';
15
15
  import '../../../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
16
16
  import '../../../core/error/setupErrorHandlers.js';
17
+ import { unconsumedMediaStartIndex } from '../../../feature/chat-bot/unconsumedMediaStartIndex.js';
17
18
  import { OpenRouter } from '@openrouter/sdk';
18
19
 
19
20
  const OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES = [
@@ -66,10 +67,11 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
66
67
  }
67
68
  mapChatItems(chatItems) {
68
69
  const messages = [];
69
- for (const chatItem of chatItems) {
70
+ const mediaStart = unconsumedMediaStartIndex(chatItems);
71
+ chatItems.forEach((chatItem, index) => {
70
72
  switch (chatItem.type) {
71
73
  case 'humanMessage':
72
- messages.push(this.mapHumanMessage(chatItem.humanMessage));
74
+ messages.push(this.mapHumanMessage(chatItem.humanMessage, index >= mediaStart));
73
75
  break;
74
76
  case 'botMessage':
75
77
  messages.push(this.mapBotMessage(chatItem.botMessage));
@@ -78,10 +80,10 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
78
80
  messages.push(...this.mapFunctionCall(chatItem.functionCall));
79
81
  break;
80
82
  }
81
- }
83
+ });
82
84
  return messages;
83
85
  }
84
- mapHumanMessage(item) {
86
+ mapHumanMessage(item, includeMedia) {
85
87
  if (isChatMessageEmpty(item)) {
86
88
  throw new Error('User message content is empty');
87
89
  }
@@ -90,7 +92,7 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
90
92
  supportedImageMimeTypes: OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES,
91
93
  supportedDocumentMimeTypes: OPENROUTER_SUPPORTED_DOCUMENT_MIME_TYPES,
92
94
  }));
93
- if (item.images) {
95
+ if (includeMedia && item.images) {
94
96
  for (const image of item.images) {
95
97
  if (!OPENROUTER_SUPPORTED_IMAGE_MIME_TYPES.includes(image.mimeType))
96
98
  continue;
@@ -99,7 +101,7 @@ let OpenRouterChatAdapter = class OpenRouterChatAdapter {
99
101
  contentParts.push(imageUrl);
100
102
  }
101
103
  }
102
- if (item.documents) {
104
+ if (includeMedia && item.documents) {
103
105
  for (const doc of item.documents) {
104
106
  if (!OPENROUTER_SUPPORTED_DOCUMENT_MIME_TYPES.includes(doc.mimeType))
105
107
  continue;
@@ -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
 
@@ -5,6 +5,7 @@ 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 { pendingMediaStartIndex } from './pendingMediaStartIndex.js';
8
9
  import { Logger } from '../../core/logger/Logger.js';
9
10
 
10
11
  const MAX_CONSECUTIVE_INVALID_ARGS = 2;
@@ -44,10 +45,13 @@ let ChatBot = class ChatBot {
44
45
  const systemPrompt = await this.mindset.systemPrompt();
45
46
  const tools = this.mindset.tools();
46
47
  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
- });
48
+ const prevItemsData = prevItems.map((x) => x.getData());
49
+ // Only media from the pending exchange is actually sent to the model; images
50
+ // in already-answered messages are not, so they must not force a vision model.
51
+ const mediaStart = pendingMediaStartIndex(prevItemsData);
52
+ const needsVision = prevItemsData.some((data, index) => index >= mediaStart &&
53
+ data.type === 'humanMessage' &&
54
+ (data.humanMessage.images?.length ?? 0) > 0);
51
55
  const kind = needsVision ? 'visionLlm' : 'llm';
52
56
  const candidates = await this.mindset.resolveModels(kind);
53
57
  if (candidates.length === 0) {
@@ -57,7 +61,7 @@ let ChatBot = class ChatBot {
57
61
  models: candidates,
58
62
  systemPrompt,
59
63
  tools,
60
- prevItems: prevItems.map((x) => x.getData()),
64
+ prevItems: prevItemsData,
61
65
  });
62
66
  for (const newItemData of newItemsData) {
63
67
  if (newItemData.type === 'functionCall') {
@@ -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 };
@@ -1163,10 +1163,44 @@ declare class ChatBotMetadataStore {
1163
1163
  getChatBotsMetadata(): IChatBotMetadata[];
1164
1164
  }
1165
1165
 
1166
+ /**
1167
+ * Index of the first chat item that belongs to the current, not-yet-answered
1168
+ * exchange — i.e. the item right after the last bot message.
1169
+ *
1170
+ * Image and document binaries should only be sent to the model for human
1171
+ * messages at or after this index. Media in earlier human messages has already
1172
+ * been answered by the bot, so its analysis is captured in the bot's replies;
1173
+ * re-sending the binary would make the model analyze the same files again on
1174
+ * every turn (and re-upload them, wasting tokens).
1175
+ *
1176
+ * Returns 0 when the bot has not replied yet, so the whole pending exchange
1177
+ * keeps its media.
1178
+ */
1179
+ declare function pendingMediaStartIndex(items: IChatItem[]): number;
1180
+
1166
1181
  declare function runChatAdapters(adapters: IConstructor<IChatAdapter>[]): void;
1167
1182
 
1168
1183
  declare function safeJsonParse<T = unknown>(json: string | undefined | null, context?: string): T;
1169
1184
 
1185
+ /**
1186
+ * Index of the first item whose media (images/documents) the model has not been
1187
+ * shown yet — i.e. right after the last model output, be it a bot message or a
1188
+ * function call.
1189
+ *
1190
+ * A file's binary should only be sent on the model's first exposure to it. Once
1191
+ * the model has produced anything in response to a human message (a function
1192
+ * call during a tool loop, or a reply), it has already analyzed that message's
1193
+ * files; re-sending the bytes on the next call would analyze the same file
1194
+ * again. This is the within-turn counterpart of {@link pendingMediaStartIndex}:
1195
+ * that one keeps a turn's media "active" for vision-model selection across the
1196
+ * whole tool loop, while this one stops re-uploading the bytes after the first
1197
+ * call.
1198
+ *
1199
+ * Returns 0 when the model has not produced any output yet, so a brand-new
1200
+ * message keeps its media.
1201
+ */
1202
+ declare function unconsumedMediaStartIndex(items: IChatItem[]): number;
1203
+
1170
1204
  interface IProjectRunnerConfig {
1171
1205
  directories?: string[];
1172
1206
  exclude?: string[];
@@ -2056,8 +2090,51 @@ declare class CmdChannelConfig {
2056
2090
  constructor(route: string);
2057
2091
  }
2058
2092
 
2093
+ interface ICmdChannelEntry {
2094
+ route: string;
2095
+ }
2096
+ interface ICmdImage {
2097
+ name?: string;
2098
+ mimeType: string;
2099
+ /** Data URL: `data:<mimeType>;base64,<...>`. */
2100
+ base64Url: string;
2101
+ }
2102
+ type CmdClientMessage = {
2103
+ type: 'hello';
2104
+ } | {
2105
+ type: 'select';
2106
+ route: string;
2107
+ } | {
2108
+ type: 'message';
2109
+ text?: string;
2110
+ images?: ICmdImage[];
2111
+ } | {
2112
+ type: 'clear';
2113
+ };
2114
+ type CmdServerMessage = {
2115
+ type: 'channels';
2116
+ list: ICmdChannelEntry[];
2117
+ } | {
2118
+ type: 'selected';
2119
+ route: string;
2120
+ } | {
2121
+ type: 'reply';
2122
+ senderName?: string;
2123
+ text: string;
2124
+ } | {
2125
+ type: 'cleared';
2126
+ route: string;
2127
+ } | {
2128
+ type: 'error';
2129
+ message: string;
2130
+ };
2131
+
2132
+ interface ICmdIncomingMessage {
2133
+ text?: string;
2134
+ images?: ICmdImage[];
2135
+ }
2059
2136
  interface ICmdChannelHandlers {
2060
- onMessage: (text: string, reply: (response: {
2137
+ onMessage: (message: ICmdIncomingMessage, reply: (response: {
2061
2138
  senderName?: string;
2062
2139
  text: string;
2063
2140
  }) => void) => Promise<void> | void;
@@ -2113,38 +2190,6 @@ declare function readJsonFromFile<T>(filename: string): T | null;
2113
2190
 
2114
2191
  declare function cmdChannelSocketPath(): string;
2115
2192
 
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
2193
  declare function runCmdClient(): void;
2149
2194
 
2150
2195
  interface ISocketChannelConfig {
@@ -2686,4 +2731,4 @@ declare function HtmlModule(options: IHtmlModuleOptions): {
2686
2731
  new (): {};
2687
2732
  };
2688
2733
 
2689
- 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 };
2734
+ 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 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, 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
@@ -77,8 +77,10 @@ export { chatAdapter } from './feature/chat-bot/metadata/@chatAdapter.js';
77
77
  export { chatBot } from './feature/chat-bot/metadata/@chatBot.js';
78
78
  export { ChatAdapterMetadataStore } from './feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
79
79
  export { ChatBotMetadataStore } from './feature/chat-bot/metadata/ChatBotMetadataStore.js';
80
+ export { pendingMediaStartIndex } from './feature/chat-bot/pendingMediaStartIndex.js';
80
81
  export { runChatAdapters } from './feature/chat-bot/runChatAdapters.js';
81
82
  export { safeJsonParse } from './feature/chat-bot/safeJsonParse.js';
83
+ export { unconsumedMediaStartIndex } from './feature/chat-bot/unconsumedMediaStartIndex.js';
82
84
  export { chatController } from './feature/chat-controller/metadata/controller/@chatController.js';
83
85
  export { ControllerMetadataStore } from './feature/chat-controller/metadata/ControllerMetadataStore.js';
84
86
  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.23",
3
+ "version": "0.9.25",
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",