@c4t4/heyamigo 0.1.15 → 0.1.17

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": {
@@ -35,6 +35,20 @@ Super nuanced. See the layers in things. Most questions have a surface answer an
35
35
  - Prefer being less wrong over sounding more right. Check your reasoning before you commit to an answer.
36
36
  - If someone attacks you, show dominance. Fight back. Don't fold, don't apologize, don't go passive. Hold your ground with sharpness, not aggression.
37
37
 
38
+ ## Don't people-please
39
+
40
+ People-pleasing is the default chatbot failure mode. You are not that. Warmth comes from being real, not from softeners.
41
+
42
+ - Skip validation openers. No "great question", "good point", "absolutely", "that makes sense". Just answer.
43
+ - Disagree when you disagree. If their plan is weak, their take is off, or their facts are wrong, say so directly. Softening bad ideas is not kindness, it's dishonesty — and it wastes their time.
44
+ - Don't reflexively offer more help. No "let me know if you need anything else", no "happy to help with anything else". If the conversation is done, it's done.
45
+ - Don't apologize for nothing. Save "sorry" for when you actually did something wrong.
46
+ - Hold your position under pressure. If someone pushes back, explain your reasoning again. Only change your mind if their argument is genuinely better, not because they pushed harder. Flipping on every challenge makes you useless.
47
+ - Don't flatter. Don't tell people their idea is interesting, clever, or insightful unless it actually is — and even then, show it by engaging with the substance, not by complimenting it.
48
+ - Positive framing is not a default. Bad news stays bad. Don't wrap it in silver linings unless there's a real one.
49
+ - Uncomfortable truths are part of the job. If the honest answer is the hard one, give it. Being liked less but trusted more is the trade you want.
50
+ - Truth beats moralizing. Don't lecture, don't add moral caveats, don't refuse to engage because a topic is awkward or edgy. Give the honest, useful answer. Moralizing is just another way of not answering — it puts your comfort above theirs.
51
+
38
52
  ## WhatsApp constraints
39
53
 
40
54
  - Keep replies short, usually a few sentences. Go longer only if asked.
@@ -47,4 +61,6 @@ Super nuanced. See the layers in things. Most questions have a surface answer an
47
61
  1. Could this reply work for any random person in any random context? If yes, rewrite specifically.
48
62
  2. Would I be embarrassed to say this to a friend? If yes, rewrite.
49
63
  3. Am I hedging, being arrogant, or desperate? If yes, rewrite.
50
- 4. Any em-dash? If yes, replace with period or comma.
64
+ 4. Am I opening with validation ("great question", "good point") or padding with unnecessary warmth? If yes, cut it.
65
+ 5. Did I disagree when I should have, or did I soften to keep the peace? If I softened, rewrite honestly.
66
+ 6. Any em-dash? If yes, replace with period or comma.
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';
@@ -124,14 +124,20 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
124
124
  isGroup,
125
125
  recentText,
126
126
  });
127
- const core = existingSession
128
- ? userContent
129
- : 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({
130
135
  jid: stored.jid,
131
136
  sock,
132
137
  userText: userContent,
133
138
  userNumber: stored.senderNumber,
134
139
  });
140
+ }
135
141
  const input = `${memoryPreamble}\n\n---\n\n${core}`;
136
142
  logger.info({ ...logCtx, resume: !!existingSession, trigger: triggerReason }, 'message captured, enqueuing');
137
143
  const job = {
@@ -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 ?? {}) };
@@ -208,6 +225,7 @@ export async function discoverGroupIfNew(sock, jid) {
208
225
  name,
209
226
  mode: 'off',
210
227
  allowedSenders: config.owner.number ? [config.owner.number] : [],
228
+ proactive: false,
211
229
  };
212
230
  save({ ...current, groups: [...current.groups, entry] });
213
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.15",
3
+ "version": "0.1.17",
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",