@c4t4/heyamigo 0.8.3 → 0.8.4

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.
@@ -206,7 +206,7 @@ If you see `[Async tasks in progress]` in your preamble, a worker is already run
206
206
 
207
207
  ## Sending files
208
208
 
209
- To send a file (screenshot, image, video, PDF, audio) to the chat, save it to `storage/temp/` and include this tag in your reply:
209
+ To send a file (screenshot, image, video, PDF, audio) to the chat, save it to `storage/outbox/` and include this tag in your reply:
210
210
 
211
211
  ```
212
212
  [FILE: /absolute/path/to/file.png]
@@ -216,7 +216,7 @@ Aliases (all behave the same): `[IMAGE: path]`, `[VIDEO: path]`, `[AUDIO: path]`
216
216
 
217
217
  Rules:
218
218
  - Always use absolute paths.
219
- - Always save under `storage/temp/`. Never save to the project root or anywhere else. Files are auto-deleted after sending.
219
+ - Always save under `storage/outbox/`. Never save to the project root or anywhere else. Files are auto-deleted after sending.
220
220
  - Media type is detected from the file extension.
221
221
  - If you send a single file with a short text reply (under 1000 chars, non-audio), the text becomes the caption.
222
222
 
@@ -226,4 +226,4 @@ A shared Chrome runs on the server at `localhost:9222` with the owner's real ses
226
226
 
227
227
  **Never call `browser_*` / `mcp__*playwright*` tools inline.** All browser work goes via `[ASYNC-BROWSER:...]`. See the two-track section above.
228
228
 
229
- To send a screenshot back: the browser worker takes it (saving to `storage/temp/`), then includes `[IMAGE: /absolute/path.png]` in its result message.
229
+ To send a screenshot back: the browser worker takes it (saving to `storage/outbox/`), then includes `[IMAGE: /absolute/path.png]` in its result message.
@@ -165,8 +165,10 @@ function compactTokens(n) {
165
165
  return `${Math.round(n / 1000)}k`;
166
166
  }
167
167
  // Proactive outbound: send a message to a chat without an incoming trigger.
168
- // Chunks, persists to the message log, never throws. Callers are responsible
169
- // for the canSendProactive() gate this function does not re-check it.
168
+ // Extracts [FILE:]/[IMAGE:]/etc tags the same way handleReply does files
169
+ // get sent as WhatsApp media, remaining text sent normally. Chunks, persists
170
+ // to the message log, never throws. Callers are responsible for the
171
+ // canSendProactive() gate — this function does not re-check it.
170
172
  export async function initiate(params) {
171
173
  const sock = getSocket();
172
174
  if (!sock) {
@@ -176,26 +178,68 @@ export async function initiate(params) {
176
178
  const raw = params.text.replaceAll('—', ', ').replaceAll('–', '-');
177
179
  if (!raw.trim())
178
180
  return false;
181
+ const { text, files } = extractFiles(raw);
179
182
  try {
180
- const chunks = chunkText(raw, config.reply.chunkChars);
181
- for (let i = 0; i < chunks.length; i++) {
182
- const chunk = chunks[i];
183
- await sendText(sock, params.jid, chunk);
183
+ // Send any files first — images, video, PDFs, audio, etc.
184
+ for (const filePath of files) {
185
+ const isFirst = filePath === files[0];
186
+ const mediaType = detectMediaType(filePath);
187
+ const supportsCaption = mediaType !== 'audio';
188
+ const caption = isFirst &&
189
+ text &&
190
+ text.length <= 1000 &&
191
+ files.length === 1 &&
192
+ supportsCaption
193
+ ? text
194
+ : undefined;
195
+ await sendFile(sock, params.jid, filePath, caption);
184
196
  await append({
185
- id: `initiate-${Date.now()}-${i}`,
197
+ id: `initiate-file-${Date.now()}`,
186
198
  jid: params.jid,
187
199
  direction: 'out',
188
200
  fromMe: true,
189
201
  sender: sock.user?.id ?? '',
190
202
  senderNumber: config.owner.number,
191
203
  timestamp: Math.floor(Date.now() / 1000),
192
- text: chunk,
193
- messageType: 'conversation',
204
+ text: caption || `[${mediaType}: ${filePath}]`,
205
+ messageType: `${mediaType}Message`,
206
+ mediaPath: filePath,
207
+ mediaType,
194
208
  });
195
- if (i < chunks.length - 1)
209
+ logger.info({ jid: params.jid, path: filePath, mediaType }, 'proactive file sent');
210
+ try {
211
+ unlinkSync(filePath);
212
+ }
213
+ catch { }
214
+ if (files.length > 1)
196
215
  await sleep(config.reply.chunkDelayMs);
197
216
  }
198
- logger.info({ jid: params.jid, chars: raw.length }, 'proactive message sent');
217
+ // Send text skip only when it was used as the caption on a single file
218
+ const textAlreadySent = files.length === 1 &&
219
+ text &&
220
+ text.length <= 1000 &&
221
+ detectMediaType(files[0]) !== 'audio';
222
+ if (text && !textAlreadySent) {
223
+ const chunks = chunkText(text, config.reply.chunkChars);
224
+ for (let i = 0; i < chunks.length; i++) {
225
+ const chunk = chunks[i];
226
+ await sendText(sock, params.jid, chunk);
227
+ await append({
228
+ id: `initiate-${Date.now()}-${i}`,
229
+ jid: params.jid,
230
+ direction: 'out',
231
+ fromMe: true,
232
+ sender: sock.user?.id ?? '',
233
+ senderNumber: config.owner.number,
234
+ timestamp: Math.floor(Date.now() / 1000),
235
+ text: chunk,
236
+ messageType: 'conversation',
237
+ });
238
+ if (i < chunks.length - 1)
239
+ await sleep(config.reply.chunkDelayMs);
240
+ }
241
+ }
242
+ logger.info({ jid: params.jid, files: files.length, chars: text.length }, 'proactive message sent');
199
243
  return true;
200
244
  }
201
245
  catch (err) {
@@ -61,7 +61,7 @@ export function buildMemoryPreamble(params) {
61
61
  'The tag will be stripped from the message. Use absolute paths only.\n\n' +
62
62
  'Browser (Playwright MCP): a real Chrome at localhost:9222 with the owner\'s sessions logged in (TikTok, Instagram, etc.). DO NOT call browser tools yourself — they belong to the BROWSER TRACK, a parallel Claude worker with its own persistent session on that Chrome. ' +
63
63
  'When a request needs browser work: send a short ack AND append [ASYNC-BROWSER: <self-sufficient task description>] at the END of your reply. The browser worker picks it up, does the work in the logged-in Chrome, sends the result back to this chat as a new message. Single URL, quick check, full scrape — all go via [ASYNC-BROWSER:...]. No exceptions.\n\n' +
64
- 'File storage: if you need to save any files (screenshots, research, notes), always save them to storage/temp/. Never save files to the project root.');
64
+ 'File storage: if you need to save files to send to the chat (screenshots, downloaded media), save them to storage/outbox/ — they auto-delete after send. For scratch/research/notes that should not be sent, use storage/temp/. Never save to the project root.');
65
65
  // Critical section
66
66
  sections.push(buildCriticalSection({
67
67
  senderNumber: params.senderNumber,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",