@c4t4/heyamigo 0.8.2 → 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.
- package/config/memory-instructions.md +3 -3
- package/dist/gateway/outgoing.js +55 -11
- package/dist/memory/preamble.js +1 -1
- package/dist/store/media.js +29 -1
- package/package.json +1 -1
|
@@ -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/
|
|
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/
|
|
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/
|
|
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.
|
package/dist/gateway/outgoing.js
CHANGED
|
@@ -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
|
-
//
|
|
169
|
-
//
|
|
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
|
-
|
|
181
|
-
for (
|
|
182
|
-
const
|
|
183
|
-
|
|
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()}
|
|
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:
|
|
193
|
-
messageType:
|
|
204
|
+
text: caption || `[${mediaType}: ${filePath}]`,
|
|
205
|
+
messageType: `${mediaType}Message`,
|
|
206
|
+
mediaPath: filePath,
|
|
207
|
+
mediaType,
|
|
194
208
|
});
|
|
195
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/memory/preamble.js
CHANGED
|
@@ -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
|
|
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/dist/store/media.js
CHANGED
|
@@ -24,13 +24,41 @@ export function detectMediaType(msg) {
|
|
|
24
24
|
return null;
|
|
25
25
|
return MEDIA_TYPES[type] ?? null;
|
|
26
26
|
}
|
|
27
|
+
// Baileys' extensionForMediaMessage throws when the media's mimetype is
|
|
28
|
+
// undefined — happens on some forwarded documents (notably PDFs shared in
|
|
29
|
+
// contexts that strip metadata). Fall back to the filename's extension,
|
|
30
|
+
// then to .bin. Never throws.
|
|
31
|
+
function resolveMediaExtension(msg) {
|
|
32
|
+
try {
|
|
33
|
+
const ext = extensionForMediaMessage(msg.message);
|
|
34
|
+
if (ext)
|
|
35
|
+
return ext;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// baileys tripped on missing mimetype — try fileName instead
|
|
39
|
+
}
|
|
40
|
+
const content = msg.message;
|
|
41
|
+
if (content) {
|
|
42
|
+
const type = getContentType(content);
|
|
43
|
+
if (type) {
|
|
44
|
+
const mediaMsg = content[type];
|
|
45
|
+
const fileName = mediaMsg?.fileName;
|
|
46
|
+
if (fileName) {
|
|
47
|
+
const m = fileName.match(/\.([a-zA-Z0-9]+)$/);
|
|
48
|
+
if (m && m[1])
|
|
49
|
+
return m[1].toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return 'bin';
|
|
54
|
+
}
|
|
27
55
|
export async function downloadAndSave(msg, jid) {
|
|
28
56
|
const mediaType = detectMediaType(msg);
|
|
29
57
|
if (!mediaType)
|
|
30
58
|
return null;
|
|
31
59
|
try {
|
|
32
60
|
const buffer = await downloadMediaMessage(msg, 'buffer', {});
|
|
33
|
-
const ext =
|
|
61
|
+
const ext = resolveMediaExtension(msg);
|
|
34
62
|
const id = msg.key.id || `${Date.now()}`;
|
|
35
63
|
const dir = mediaDir(jid);
|
|
36
64
|
await mkdir(dir, { recursive: true });
|