@c4t4/heyamigo 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ const KINDS = [
14
14
  'JOURNAL-NEW',
15
15
  'ASYNC',
16
16
  'ASYNC-BROWSER',
17
+ 'SEND-TEXT',
17
18
  ];
18
19
  // Walk backwards from the end of the string, tracking bracket depth, to find
19
20
  // the `[` that matches the final `]`. Returns the tag kind, its payload, and
@@ -74,6 +75,7 @@ export function extractFlags(reply) {
74
75
  const journalCreates = [];
75
76
  const asyncTasks = [];
76
77
  const asyncBrowserTasks = [];
78
+ const sendTexts = [];
77
79
  while (true) {
78
80
  const peeled = peelTrailingTag(current);
79
81
  if (!peeled)
@@ -105,6 +107,11 @@ export function extractFlags(reply) {
105
107
  asyncBrowserTasks.unshift({ description: payload });
106
108
  }
107
109
  }
110
+ else if (kind === 'SEND-TEXT') {
111
+ const parsed = parseSendTextPayload(payload);
112
+ if (parsed)
113
+ sendTexts.unshift(parsed);
114
+ }
108
115
  }
109
116
  return {
110
117
  clean: current,
@@ -113,6 +120,7 @@ export function extractFlags(reply) {
113
120
  journalCreates,
114
121
  asyncTasks,
115
122
  asyncBrowserTasks,
123
+ sendTexts,
116
124
  };
117
125
  }
118
126
  // Legacy helper kept so existing callers still compile.
@@ -121,6 +129,26 @@ export function extractDigestFlag(reply) {
121
129
  return { clean: r.clean, flag: r.digest };
122
130
  }
123
131
  const JOURNAL_SEP_RE = /\s*(?:[—\-–]|:)\s*/;
132
+ // Parse `address=<addr> body="..."` style key=value payload.
133
+ // Body is delimited by double quotes; everything else by whitespace.
134
+ // Returns null if address or body is missing.
135
+ function parseSendTextPayload(payload) {
136
+ // Grab body="..." first (longest match so quoted body can contain spaces)
137
+ const bodyMatch = payload.match(/\bbody\s*=\s*"((?:[^"\\]|\\.)*)"/);
138
+ if (!bodyMatch)
139
+ return null;
140
+ const body = bodyMatch[1]
141
+ .replace(/\\"/g, '"')
142
+ .replace(/\\\\/g, '\\');
143
+ if (!body.trim())
144
+ return null;
145
+ // Strip the body=... portion so address parsing doesn't trip on it
146
+ const withoutBody = payload.replace(bodyMatch[0], '').trim();
147
+ const addrMatch = withoutBody.match(/\baddress\s*=\s*([^\s]+)/);
148
+ if (!addrMatch)
149
+ return null;
150
+ return { address: addrMatch[1], body };
151
+ }
124
152
  function parseJournalPayload(payload) {
125
153
  // Split on first em-dash, en-dash, hyphen, or colon between slug and note.
126
154
  const match = payload.match(/^([a-zA-Z0-9][a-zA-Z0-9-]*)(.*)$/);
@@ -124,7 +124,20 @@ async function executeAsyncTask(task) {
124
124
  // as markers; clean pre-marker text is only sent to chat when short (a
125
125
  // failure explanation or tight ack) or when no markers fired at all.
126
126
  const { extractFlags } = await import('../memory/digest-flag.js');
127
- const { clean, digest, journals, journalCreates } = extractFlags(output);
127
+ const { clean, digest, journals, journalCreates, sendTexts } = extractFlags(output);
128
+ // SEND-TEXT: async task wants to text a different chat too.
129
+ if (sendTexts.length > 0) {
130
+ const { enqueueOutbound } = await import('./outbound.js');
131
+ for (let i = 0; i < sendTexts.length; i++) {
132
+ const t = sendTexts[i];
133
+ enqueueOutbound({
134
+ address: t.address,
135
+ kind: 'text',
136
+ text: t.body,
137
+ idempotencyKey: `async-sendtext-${task.id}-${i}`,
138
+ });
139
+ }
140
+ }
128
141
  // Journal creates run first so an entry flagged in the same output against
129
142
  // a new slug lands correctly.
130
143
  const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
@@ -402,7 +415,19 @@ async function runBrowserTask(task) {
402
415
  }
403
416
  // Route markers the same way the general async lane does.
404
417
  const { extractFlags } = await import('../memory/digest-flag.js');
405
- const { clean, digest, journals, journalCreates } = extractFlags(reply);
418
+ const { clean, digest, journals, journalCreates, sendTexts } = extractFlags(reply);
419
+ if (sendTexts.length > 0) {
420
+ const { enqueueOutbound } = await import('./outbound.js');
421
+ for (let i = 0; i < sendTexts.length; i++) {
422
+ const t = sendTexts[i];
423
+ enqueueOutbound({
424
+ address: t.address,
425
+ kind: 'text',
426
+ text: t.body,
427
+ idempotencyKey: `browser-sendtext-${task.id}-${i}`,
428
+ });
429
+ }
430
+ }
406
431
  const { appendEntry, createJournal, getJournal, isValidSlug } = await import('../memory/journals.js');
407
432
  for (const op of journalCreates) {
408
433
  if (!isValidSlug(op.slug))
@@ -7,6 +7,7 @@ import { extractFlags } from '../memory/digest-flag.js';
7
7
  import { appendEntry, createJournal, getJournal, isValidSlug, } from '../memory/journals.js';
8
8
  import { scheduleDigest } from '../memory/scheduler.js';
9
9
  import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
10
+ import { enqueueOutbound } from './outbound.js';
10
11
  function isStaleSessionError(err) {
11
12
  return (err instanceof Error &&
12
13
  err.message.includes('No conversation found'));
@@ -39,7 +40,7 @@ async function callClaude(job) {
39
40
  if (job.senderNumber) {
40
41
  addDailyTokens(job.senderNumber, usage.inputTokens + usage.outputTokens);
41
42
  }
42
- const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, } = extractFlags(reply);
43
+ const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, } = extractFlags(reply);
43
44
  if (digest) {
44
45
  logger.info({ jid: job.jid, number: job.senderNumber, reason: digest }, 'DIGEST flag raised, scheduling');
45
46
  scheduleDigest({
@@ -105,6 +106,19 @@ async function callClaude(job) {
105
106
  allowedTools: job.allowedTools ?? 'all',
106
107
  });
107
108
  }
109
+ // SEND-TEXT: cross-chat text send. Agent specified the destination
110
+ // address explicitly. Just drops a row in outbound; sender worker
111
+ // dispatches by channel.
112
+ for (let i = 0; i < sendTexts.length; i++) {
113
+ const t = sendTexts[i];
114
+ enqueueOutbound({
115
+ address: t.address,
116
+ kind: 'text',
117
+ text: t.body,
118
+ idempotencyKey: `sendtext-${job.jid}-${Date.now()}-${i}`,
119
+ });
120
+ logger.info({ from: job.jid, to: t.address, chars: t.body.length }, 'SEND-TEXT enqueued');
121
+ }
108
122
  return {
109
123
  reply: clean,
110
124
  stats: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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",