@c4t4/heyamigo 0.1.18 → 0.5.0

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.
@@ -1,8 +1,10 @@
1
1
  import { askClaude } from '../ai/claude.js';
2
2
  import { clearSession, setSession, setUsage } from '../ai/sessions.js';
3
3
  import { logger } from '../logger.js';
4
- import { extractDigestFlag } from '../memory/digest-flag.js';
4
+ import { extractFlags } from '../memory/digest-flag.js';
5
+ import { appendEntry, createJournal, getJournal, isValidSlug, } from '../memory/journals.js';
5
6
  import { scheduleDigest } from '../memory/scheduler.js';
7
+ import { enqueueAsyncTask } from './async-tasks.js';
6
8
  function isStaleSessionError(err) {
7
9
  return (err instanceof Error &&
8
10
  err.message.includes('No conversation found'));
@@ -25,17 +27,70 @@ async function callClaude(job) {
25
27
  totalContextTokens,
26
28
  updatedAt: Math.floor(Date.now() / 1000),
27
29
  });
28
- const { clean, flag } = extractDigestFlag(reply);
29
- if (flag) {
30
- logger.info({ jid: job.jid, number: job.senderNumber, reason: flag }, 'DIGEST flag raised, scheduling');
30
+ const { clean, digest, journals, journalCreates, asyncTasks } = extractFlags(reply);
31
+ if (digest) {
32
+ logger.info({ jid: job.jid, number: job.senderNumber, reason: digest }, 'DIGEST flag raised, scheduling');
31
33
  scheduleDigest({
32
34
  jid: job.jid,
33
35
  number: job.senderNumber,
34
- reason: flag,
36
+ reason: digest,
37
+ });
38
+ }
39
+ // Creates run BEFORE entry appends so that a reply creating a new journal
40
+ // AND flagging its first entry in the same turn works correctly.
41
+ for (const op of journalCreates) {
42
+ if (!isValidSlug(op.slug)) {
43
+ logger.warn({ op, jid: job.jid }, 'JOURNAL-NEW: invalid slug, dropped');
44
+ continue;
45
+ }
46
+ try {
47
+ if (getJournal(op.slug)) {
48
+ logger.info({ slug: op.slug }, 'JOURNAL-NEW for existing slug, ignored');
49
+ continue;
50
+ }
51
+ createJournal({
52
+ slug: op.slug,
53
+ name: titleCase(op.slug),
54
+ purpose: op.purpose,
55
+ });
56
+ logger.info({ slug: op.slug, jid: job.jid }, 'journal created via bot marker');
57
+ }
58
+ catch (err) {
59
+ logger.error({ err, op, jid: job.jid }, 'JOURNAL-NEW failed');
60
+ }
61
+ }
62
+ for (const j of journals) {
63
+ const ok = appendEntry(j.slug, {
64
+ source: 'reactive',
65
+ jid: job.jid,
66
+ senderNumber: job.senderNumber,
67
+ note: j.note,
68
+ });
69
+ if (!ok) {
70
+ logger.warn({ slug: j.slug, jid: job.jid }, 'JOURNAL flag pointed at unknown slug, dropped');
71
+ }
72
+ }
73
+ // Async tasks: Claude delegated long work (browser scrapes, multi-step
74
+ // research, etc.) to the background lane. The clean reply above is the
75
+ // user-facing ack and will be sent normally. The async tasks run stateless
76
+ // in their own queue and report back via initiate() when done.
77
+ for (const t of asyncTasks) {
78
+ enqueueAsyncTask({
79
+ jid: job.jid,
80
+ senderNumber: job.senderNumber,
81
+ description: t.description,
82
+ originatingMessage: job.text,
83
+ allowedTools: job.allowedTools ?? 'all',
35
84
  });
36
85
  }
37
86
  return { reply: clean };
38
87
  }
88
+ function titleCase(slug) {
89
+ return slug
90
+ .split('-')
91
+ .map((p) => (p ? p[0].toUpperCase() + p.slice(1) : p))
92
+ .join(' ');
93
+ }
39
94
  export async function processJob(job) {
40
95
  try {
41
96
  return await callClaude(job);
@@ -96,9 +96,11 @@ function save(next) {
96
96
  export function getAccess() {
97
97
  return current;
98
98
  }
99
- // Guardrail for proactive (unsolicited) messaging. Returns true ONLY if the
100
- // target chat has an explicit proactive:true entry. Default is deny — no
101
- // journal/scheduler/observer may message a chat without explicit opt-in.
99
+ // Guardrail for proactive (unsolicited) messaging. Default deny.
100
+ //
101
+ // Exception: the owner's own self-DM is always allowed — the owner implicitly
102
+ // consents to the bot nudging them in their own DM. Other DMs and groups
103
+ // require an explicit `proactive: true` entry in access.json.
102
104
  export function canSendProactive(jid) {
103
105
  const isGroup = jid.endsWith('@g.us');
104
106
  if (isGroup) {
@@ -108,6 +110,9 @@ export function canSendProactive(jid) {
108
110
  const number = jidDecode(jid)?.user;
109
111
  if (!number)
110
112
  return false;
113
+ // Owner's self-DM is always allowed.
114
+ if (config.owner.number && number === config.owner.number)
115
+ return true;
111
116
  const entry = current.dms.allowed.find((d) => d.number === number);
112
117
  return entry?.proactive === true;
113
118
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.1.18",
3
+ "version": "0.5.0",
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",