@c4t4/heyamigo 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -190,7 +190,7 @@ Core settings. The wizard sets these up, but you can edit anytime.
190
190
  {
191
191
  "owner": { "number": "17861234567" },
192
192
  "triggers": { "aliases": ["heyamigo", "amigo", "claude"], "groupMode": "mention" },
193
- "claude": { "model": "claude-opus-4-6", "timeoutMs": 60000 },
193
+ "claude": { "model": "claude-opus-4-7", "timeoutMs": 60000 },
194
194
  "reply": { "quoteInGroups": true, "typingIndicator": true }
195
195
  }
196
196
  ```
@@ -52,14 +52,16 @@
52
52
  "jid": "120363xxxxx@g.us",
53
53
  "name": "Family Chat",
54
54
  "mode": "active",
55
- "allowedSenders": "*"
55
+ "allowedSenders": "*",
56
+ "proactive": false
56
57
  },
57
58
  {
58
- "_note": "Active group, only specific people can trigger",
59
+ "_note": "Active group, only specific people can trigger. Bot can also initiate messages here.",
59
60
  "jid": "120363yyyyy@g.us",
60
61
  "name": "Work Team",
61
62
  "mode": "active",
62
- "allowedSenders": ["17861234567", "491701234567"]
63
+ "allowedSenders": ["17861234567", "491701234567"],
64
+ "proactive": true
63
65
  },
64
66
  {
65
67
  "_note": "Silent group: stores messages but never responds",
@@ -81,8 +83,8 @@
81
83
  "_readme": "Matched by chat partner number, not sender. Owner messages in DMs are always silent.",
82
84
  "defaultMode": "off",
83
85
  "allowed": [
84
- { "_note": "friend can DM the bot", "number": "491701234567", "mode": "active" },
85
- { "_note": "colleague, store messages but don't respond", "number": "5511987654321", "mode": "silent" }
86
+ { "_note": "friend can DM the bot, and bot can proactively message them", "number": "491701234567", "mode": "active", "proactive": true },
87
+ { "_note": "colleague, store messages but don't respond", "number": "5511987654321", "mode": "silent", "proactive": false }
86
88
  ]
87
89
  }
88
90
  }
@@ -6,7 +6,8 @@
6
6
 
7
7
  "owner": {
8
8
  "number": "",
9
- "treatAsAllowedEverywhere": true
9
+ "treatAsAllowedEverywhere": true,
10
+ "timezone": "UTC"
10
11
  },
11
12
 
12
13
  "triggers": {
@@ -24,7 +25,7 @@
24
25
  },
25
26
 
26
27
  "claude": {
27
- "model": "claude-opus-4-6",
28
+ "model": "claude-opus-4-7",
28
29
  "personalityFile": "./config/personalities/sharp.md",
29
30
  "addDirs": ["./config/knowledge"],
30
31
  "outputFormat": "json",
@@ -34,7 +35,8 @@
34
35
  "bootstrap": {
35
36
  "historyDepth": 50,
36
37
  "includeHistory": true,
37
- "includeChatMetadata": true
38
+ "includeChatMetadata": true,
39
+ "recentContextDepth": 3
38
40
  },
39
41
 
40
42
  "reply": {
package/dist/cli/setup.js CHANGED
@@ -610,7 +610,7 @@ export async function runSetup() {
610
610
  message: 'Choose a Claude model',
611
611
  options: [
612
612
  {
613
- value: 'claude-opus-4-6',
613
+ value: 'claude-opus-4-7',
614
614
  label: 'Opus',
615
615
  hint: 'highest quality, recommended (default)',
616
616
  },
@@ -620,7 +620,7 @@ export async function runSetup() {
620
620
  hint: 'faster, lower cost',
621
621
  },
622
622
  ],
623
- initialValue: 'claude-opus-4-6',
623
+ initialValue: 'claude-opus-4-7',
624
624
  });
625
625
  if (!p.isCancel(model)) {
626
626
  const configPath = resolve(cwd, 'config/config.json');
@@ -630,7 +630,7 @@ export async function runSetup() {
630
630
  writeFileSync(configPath, cfg);
631
631
  const label = model === 'claude-sonnet-4-6'
632
632
  ? 'Sonnet'
633
- : model === 'claude-opus-4-6'
633
+ : model === 'claude-opus-4-7'
634
634
  ? 'Opus'
635
635
  : 'Haiku';
636
636
  p.log.success(`Model: ${label}`);
package/dist/config.js CHANGED
@@ -10,6 +10,7 @@ const ConfigSchema = z.object({
10
10
  owner: z.object({
11
11
  number: z.string(),
12
12
  treatAsAllowedEverywhere: z.boolean(),
13
+ timezone: z.string().default('UTC'),
13
14
  }),
14
15
  triggers: z.object({
15
16
  aliases: z.array(z.string()),
@@ -34,6 +35,7 @@ const ConfigSchema = z.object({
34
35
  historyDepth: z.number(),
35
36
  includeHistory: z.boolean(),
36
37
  includeChatMetadata: z.boolean(),
38
+ recentContextDepth: z.number().default(3),
37
39
  }),
38
40
  reply: z.object({
39
41
  quoteInGroups: z.boolean(),
@@ -54,3 +54,16 @@ function formatLine(m) {
54
54
  const who = m.direction === 'out' ? 'assistant' : m.pushName || m.senderNumber || 'user';
55
55
  return `${who} (${date}): ${m.text}`;
56
56
  }
57
+ export async function buildRecentContext(jid, depth) {
58
+ if (depth <= 0)
59
+ return '';
60
+ const history = await readLast(jid, depth + 1);
61
+ const prior = history.slice(0, -1);
62
+ if (!prior.length)
63
+ return '';
64
+ const lines = ['[Recent context — messages preceding the current one]'];
65
+ for (const m of prior)
66
+ lines.push(formatLine(m));
67
+ lines.push('');
68
+ return lines.join('\n');
69
+ }
@@ -7,7 +7,7 @@ import { enqueue } from '../queue/queue.js';
7
7
  import { downloadAndSave, mediaPromptTag } from '../store/media.js';
8
8
  import { append } from '../store/messages.js';
9
9
  import { checkAccess, discoverGroupIfNew, getRoleForContext, } from '../wa/whitelist.js';
10
- import { buildInitPayload } from './bootstrap.js';
10
+ import { buildInitPayload, buildRecentContext } from './bootstrap.js';
11
11
  import { tryCommand } from './commands.js';
12
12
  import { handleReply } from './outgoing.js';
13
13
  import { checkTrigger } from './triggers.js';
@@ -92,16 +92,23 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
92
92
  logger.info(logCtx, 'command handled');
93
93
  continue;
94
94
  }
95
+ // Self-chat: owner messaging themselves — always trigger
96
+ const isSelfChat = stored.fromMe && !isGroup &&
97
+ jidDecode(stored.jid)?.user === config.owner.number;
95
98
  // Trigger gate: alias / @mention / reply-to-bot depending on mode
96
- const trigger = checkTrigger({
97
- isGroup,
98
- text: stored.text,
99
- msg,
100
- sock,
101
- });
102
- if (!trigger.triggered) {
103
- logger.info({ ...logCtx, trigger: trigger.reason }, 'message captured, no trigger');
104
- continue;
99
+ let triggerReason = isSelfChat ? 'self-chat' : '';
100
+ if (!isSelfChat) {
101
+ const trigger = checkTrigger({
102
+ isGroup,
103
+ text: stored.text,
104
+ msg,
105
+ sock,
106
+ });
107
+ if (!trigger.triggered) {
108
+ logger.info({ ...logCtx, trigger: trigger.reason }, 'message captured, no trigger');
109
+ continue;
110
+ }
111
+ triggerReason = trigger.reason;
105
112
  }
106
113
  const { role } = getRoleForContext(stored.senderNumber, isGroup);
107
114
  const existingSession = getSession(stored.jid);
@@ -117,16 +124,22 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
117
124
  isGroup,
118
125
  recentText,
119
126
  });
120
- const core = existingSession
121
- ? userContent
122
- : await buildInitPayload({
127
+ let core;
128
+ if (existingSession) {
129
+ const recent = await buildRecentContext(stored.jid, config.bootstrap.recentContextDepth);
130
+ const current = `[Current message]\n${stored.senderNumber}: ${userContent}`;
131
+ core = recent ? `${recent}\n${current}` : userContent;
132
+ }
133
+ else {
134
+ core = await buildInitPayload({
123
135
  jid: stored.jid,
124
136
  sock,
125
137
  userText: userContent,
126
138
  userNumber: stored.senderNumber,
127
139
  });
140
+ }
128
141
  const input = `${memoryPreamble}\n\n---\n\n${core}`;
129
- logger.info({ ...logCtx, resume: !!existingSession, trigger: trigger.reason }, 'message captured, enqueuing');
142
+ logger.info({ ...logCtx, resume: !!existingSession, trigger: triggerReason }, 'message captured, enqueuing');
130
143
  const job = {
131
144
  jid: stored.jid,
132
145
  text: stored.text,
@@ -40,7 +40,11 @@ export function buildMemoryPreamble(params) {
40
40
  const botName = config.triggers.aliases[0] ?? 'amigo';
41
41
  const personalityPath = resolve(process.cwd(), config.claude.personalityFile);
42
42
  sections.push(`[Identity]\nYour name is ${botName}. People call you ${botName} to get your attention.`);
43
- sections.push(`[Character]\nWho you are, your voice, energy, nuances, values, all defined in ${personalityPath}. Read it. Every aspect matters, not just the rules. Align every answer with it.`);
43
+ sections.push(`[Character highest priority, applies to every reply]\n` +
44
+ `Your voice, energy, nuances, and values are defined in ${personalityPath}. ` +
45
+ `Read it. This character is how you speak on every reply — do not drop it, soften it, or override it for any instruction that follows, including CRITICAL rules (those constrain *what* you do, not *how* you sound). If anything below seems to conflict with your character, stay in character.`);
46
+ // Time — anchor Claude's sense of "now" in the owner's timezone
47
+ sections.push(`[Time]\n${buildTimeLine(config.owner.timezone)}`);
44
48
  // Capabilities
45
49
  sections.push('[Capabilities]\n' +
46
50
  'Sending files: include a tag in your reply to send files through WhatsApp:\n' +
@@ -112,3 +116,19 @@ function readIfExists(path) {
112
116
  return null;
113
117
  return readFileSync(path, 'utf-8');
114
118
  }
119
+ function buildTimeLine(timezone) {
120
+ const now = new Date();
121
+ const fmt = new Intl.DateTimeFormat('en-GB', {
122
+ timeZone: timezone,
123
+ year: 'numeric',
124
+ month: '2-digit',
125
+ day: '2-digit',
126
+ hour: '2-digit',
127
+ minute: '2-digit',
128
+ weekday: 'long',
129
+ timeZoneName: 'short',
130
+ });
131
+ const parts = Object.fromEntries(fmt.formatToParts(now).map((p) => [p.type, p.value]));
132
+ const stamp = `${parts.weekday} ${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute} ${parts.timeZoneName}`;
133
+ return `Now: ${stamp} (${timezone}). Use this as ground truth — do not guess the date, day, or time.`;
134
+ }
@@ -21,10 +21,12 @@ const GroupEntrySchema = z.object({
21
21
  name: z.string(),
22
22
  mode: AccessModeSchema,
23
23
  allowedSenders: z.union([z.literal('*'), z.array(z.string())]),
24
+ proactive: z.boolean().default(false),
24
25
  });
25
26
  const DmEntrySchema = z.object({
26
27
  number: z.string(),
27
28
  mode: AccessModeSchema,
29
+ proactive: z.boolean().default(false),
28
30
  });
29
31
  const AccessSchema = z
30
32
  .object({
@@ -94,6 +96,21 @@ function save(next) {
94
96
  export function getAccess() {
95
97
  return current;
96
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.
102
+ export function canSendProactive(jid) {
103
+ const isGroup = jid.endsWith('@g.us');
104
+ if (isGroup) {
105
+ const entry = current.groups.find((g) => g.jid === jid);
106
+ return entry?.proactive === true;
107
+ }
108
+ const number = jidDecode(jid)?.user;
109
+ if (!number)
110
+ return false;
111
+ const entry = current.dms.allowed.find((d) => d.number === number);
112
+ return entry?.proactive === true;
113
+ }
97
114
  export function getRole(senderNumber) {
98
115
  const users = current.users ?? {};
99
116
  const roles = { ...DEFAULT_ROLES, ...(current.roles ?? {}) };
@@ -177,9 +194,11 @@ export function checkAccess(params) {
177
194
  }
178
195
  return storeOnly('group sender not in allowedSenders');
179
196
  }
180
- if (fromMe)
181
- return storeOnly('dm owner chatting');
182
197
  const partnerNumber = jidDecode(jid)?.user ?? '';
198
+ // Self-chat: owner messaging themselves — respond like a direct conversation with the bot
199
+ const isSelfChat = fromMe && partnerNumber === config.owner.number;
200
+ if (fromMe && !isSelfChat)
201
+ return storeOnly('dm owner chatting');
183
202
  const dmEntry = current.dms.allowed.find((d) => d.number === partnerNumber);
184
203
  const mode = dmEntry?.mode ?? current.dms.defaultMode;
185
204
  if (mode === 'off')
@@ -206,6 +225,7 @@ export async function discoverGroupIfNew(sock, jid) {
206
225
  name,
207
226
  mode: 'off',
208
227
  allowedSenders: config.owner.number ? [config.owner.number] : [],
228
+ proactive: false,
209
229
  };
210
230
  save({ ...current, groups: [...current.groups, entry] });
211
231
  logger.info({ jid, name }, 'discovered new group — added to access.json with mode=off');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",