@c4t4/heyamigo 0.10.5 → 0.10.6

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
@@ -20,7 +20,7 @@ WhatsApp / Telegram ─► inbound ─► chat workers ─► outbound ─► Wh
20
20
  - **Scheduling in the sender's timezone.** Natural language → `[REMIND: 2026-05-26 09:00 — ...]` or `[CRON: 0 9 * * 1 PROMPT — ...]`. Fires at the user's wall-clock 9am, not the server's. Cron variants: deliver text, run AI, kick off async work, or drive a browser.
21
21
  - **A real Chrome.** Browser delegation via `[ASYNC-BROWSER: ...]` to a parallel provider session on a shared logged-in Chrome over CDP. TikTok, Instagram, anywhere the owner is logged in. SSH-tunneled noVNC for setup.
22
22
  - **Per-reply footer with confirmation tags.** Every side effect from the turn is visible: `_9.9s · 465k↑ 169↓ · +remind · +thread-new · +digest_`. No guessing whether a schedule actually got created.
23
- - **Default-deny proactive messaging.** Groups stay silent unless explicitly opted in. Per-role token quotas, file-size caps, tool restrictions.
23
+ - **Default-deny chat activation.** Groups and DMs only answer when their own `triggerMode` is set in `config/access.json`; missing means `off`. Per-role token quotas, file-size caps, tool restrictions.
24
24
 
25
25
  For the why behind these — claim primitives, tag-as-side-effect channel, per-category learning, provider abstraction, the trade-offs that didn't survive the first revision — see [`docs/architecture.md`](docs/architecture.md).
26
26
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "_readme": "Access rules. Copy this to access.json and customize. Groups auto-discover with mode=off when the bot sees them. WhatsApp users are phone numbers; Telegram users are tg_<user_id>.",
2
+ "_readme": "Access rules. Copy this to access.json and customize. Groups auto-discover with mode=off and triggerMode=off when the bot sees them. triggerMode is per group/DM: off, mention, command, or all. WhatsApp users are phone numbers; Telegram users are tg_<user_id>.",
3
3
 
4
4
  "_limits_readme": "maxFileBytes caps the size of media/documents sent to Claude (null = unlimited). dailyTokenLimit caps Claude tokens (input+output) per user per day in the owner's timezone (null = unlimited). The bot owner is always unlimited regardless of role.",
5
5
 
@@ -62,6 +62,7 @@
62
62
  "name": "Family Chat",
63
63
  "mode": "active",
64
64
  "allowedSenders": "*",
65
+ "triggerMode": "mention",
65
66
  "proactive": false
66
67
  },
67
68
  {
@@ -70,6 +71,7 @@
70
71
  "name": "Work Team",
71
72
  "mode": "active",
72
73
  "allowedSenders": ["17861234567", "491701234567"],
74
+ "triggerMode": "all",
73
75
  "proactive": true
74
76
  },
75
77
  {
@@ -78,6 +80,7 @@
78
80
  "name": "Telegram Team",
79
81
  "mode": "active",
80
82
  "allowedSenders": ["tg_123456789"],
83
+ "triggerMode": "mention",
81
84
  "proactive": false
82
85
  },
83
86
  {
@@ -85,24 +88,26 @@
85
88
  "jid": "120363zzzzz@g.us",
86
89
  "name": "Announcements",
87
90
  "mode": "silent",
88
- "allowedSenders": "*"
91
+ "allowedSenders": "*",
92
+ "triggerMode": "off"
89
93
  },
90
94
  {
91
95
  "_note": "Disabled group: completely ignored",
92
96
  "jid": "120363aaaaa@g.us",
93
97
  "name": "Muted Group",
94
98
  "mode": "off",
95
- "allowedSenders": "*"
99
+ "allowedSenders": "*",
100
+ "triggerMode": "off"
96
101
  }
97
102
  ],
98
103
 
99
104
  "dms": {
100
- "_readme": "Matched by chat partner number/key, not sender. Telegram DMs use tg_<user_id>. Owner messages in WhatsApp DMs are always silent.",
105
+ "_readme": "Matched by chat partner number/key, not sender. Telegram DMs use tg_<user_id>. Owner messages in WhatsApp DMs are always silent. triggerMode defaults to off when omitted.",
101
106
  "defaultMode": "off",
102
107
  "allowed": [
103
- { "_note": "friend can DM the bot, and bot can proactively message them", "number": "491701234567", "mode": "active", "proactive": true },
104
- { "_note": "Telegram friend can DM the bot", "number": "tg_123456789", "mode": "active", "proactive": true },
105
- { "_note": "colleague, store messages but don't respond", "number": "5511987654321", "mode": "silent", "proactive": false }
108
+ { "_note": "friend can DM the bot, and bot can proactively message them", "number": "491701234567", "mode": "active", "triggerMode": "all", "proactive": true },
109
+ { "_note": "Telegram friend can DM the bot", "number": "tg_123456789", "mode": "active", "triggerMode": "all", "proactive": true },
110
+ { "_note": "colleague, store messages but don't respond", "number": "5511987654321", "mode": "silent", "triggerMode": "off", "proactive": false }
106
111
  ]
107
112
  }
108
113
  }
@@ -19,8 +19,6 @@
19
19
 
20
20
  "triggers": {
21
21
  "aliases": ["heyamigo", "amigo", "claude", "clawd", "grok", "codex", "xai"],
22
- "groupMode": "mention",
23
- "dmMode": "mention",
24
22
  "replyToBotCounts": true
25
23
  },
26
24
 
package/dist/cli/setup.js CHANGED
@@ -706,7 +706,7 @@ export async function runSetup() {
706
706
  }
707
707
  }
708
708
  // ── Name your amigo ───────────────────────────────────────────
709
- p.log.info('Give your amigo a name. People mention this name in a message to get a reply. ' +
709
+ p.log.info('Give your amigo a name. In chats with triggerMode: "mention", people mention this name to get a reply. ' +
710
710
  'You can add multiple names separated by commas.');
711
711
  const nameInput = await p.text({
712
712
  message: 'What should your amigo be called?',
@@ -726,7 +726,7 @@ export async function runSetup() {
726
726
  let cfg = readFileSync(cfgPath, 'utf-8');
727
727
  cfg = cfg.replace(/"aliases":\s*\[.*?\]/, `"aliases": ${JSON.stringify(aliases)}`);
728
728
  writeFileSync(cfgPath, cfg);
729
- p.log.success(`Your amigo responds to: ${names.join(', ')}`);
729
+ p.log.success(`Mention names: ${names.join(', ')}`);
730
730
  }
731
731
  }
732
732
  }
@@ -737,7 +737,9 @@ export async function runSetup() {
737
737
  ' 2. New groups start with mode: "off" (bot stays silent).\n' +
738
738
  ' To activate: edit config/access.json, change mode to "active".\n\n' +
739
739
  ' 3. Set allowedSenders to "*" (everyone) or specific numbers.\n\n' +
740
- ' 4. Once active, mention the bot\'s name in a message to get a reply.\n\n' +
740
+ ' 4. Set triggerMode per chat: "mention", "all", "command", or "off".\n' +
741
+ ' Missing triggerMode means "off".\n\n' +
742
+ ' 5. With triggerMode "mention", mention the bot\'s name to get a reply.\n\n' +
741
743
  'DMs work the same way — add numbers to dms.allowed in access.json.');
742
744
  // Auto-add owner as admin if we have the number
743
745
  if (ownerNum) {
@@ -832,8 +834,8 @@ export async function runSetup() {
832
834
  ' npx @c4t4/heyamigo stop / restart / status',
833
835
  '',
834
836
  'Configuration:',
835
- ' config/config.json — triggers, model',
836
- ' config/access.json — groups, DMs, roles',
837
+ ' config/config.json — bot name, model',
838
+ ' config/access.json — groups, DMs, roles, per-chat triggerMode',
837
839
  ].join('\n'), 'Setup complete!');
838
840
  p.log.warning('IMPORTANT: The bot won\'t respond until you activate a group!\n\n' +
839
841
  ' Step 1 — Start the bot:\n' +
@@ -844,9 +846,10 @@ export async function runSetup() {
844
846
  ' nano config/access.json\n' +
845
847
  ' - Find the group, change mode from "off" to "active"\n' +
846
848
  ' - Set allowedSenders to "*" for everyone\n\n' +
849
+ ' - Set triggerMode to "mention" or "all"\n\n' +
847
850
  ' Step 4 — Restart the bot:\n' +
848
851
  ' npx @c4t4/heyamigo restart\n\n' +
849
- ' Step 5 — Mention the bot\'s name in the group to get a reply.\n\n' +
852
+ ' Step 5 — If triggerMode is "mention", mention the bot\'s name in the group to get a reply.\n\n' +
850
853
  ' Debugging:\n' +
851
854
  ' npx @c4t4/heyamigo logs');
852
855
  p.log.info('TIP: Track your bot\'s memory with git.\n' +
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { z } from 'zod';
4
- const TriggerModeSchema = z.enum(['all', 'mention', 'command', 'off']);
4
+ export const TriggerModeSchema = z.enum(['all', 'mention', 'command', 'off']);
5
5
  const ConfigSchema = z.object({
6
6
  whatsapp: z.object({
7
7
  enabled: z.boolean().default(true),
@@ -25,8 +25,6 @@ const ConfigSchema = z.object({
25
25
  }),
26
26
  triggers: z.object({
27
27
  aliases: z.array(z.string()),
28
- groupMode: TriggerModeSchema,
29
- dmMode: TriggerModeSchema,
30
28
  replyToBotCounts: z.boolean(),
31
29
  }),
32
30
  commands: z.object({
@@ -141,7 +141,7 @@ export async function processIncomingMessage(incoming, opts = {}) {
141
141
  let triggerReason = incoming.selfChat ? 'self-chat' : '';
142
142
  if (!incoming.selfChat) {
143
143
  const trigger = checkTrigger({
144
- isGroup: incoming.isGroup,
144
+ mode: decision.triggerMode,
145
145
  text: stored.text,
146
146
  mentionedBot: incoming.triggerHints?.mentionedBot,
147
147
  replyToBot: incoming.triggerHints?.replyToBot,
@@ -11,10 +11,7 @@ function aliasMatches(text, aliases) {
11
11
  return null;
12
12
  }
13
13
  export function checkTrigger(params) {
14
- const { isGroup, text } = params;
15
- const mode = isGroup
16
- ? config.triggers.groupMode
17
- : config.triggers.dmMode;
14
+ const { mode, text } = params;
18
15
  if (mode === 'off')
19
16
  return { triggered: false, reason: 'mode=off' };
20
17
  if (mode === 'all')
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { jidDecode } from 'baileys';
4
4
  import { z } from 'zod';
5
- import { config } from '../config.js';
5
+ import { config, TriggerModeSchema } from '../config.js';
6
6
  import { actorKeyFromAddress, parseAddress } from '../db/address.js';
7
7
  import { logger } from '../logger.js';
8
8
  const AccessModeSchema = z.enum(['off', 'silent', 'active']);
@@ -46,11 +46,13 @@ const GroupEntrySchema = z.object({
46
46
  name: z.string(),
47
47
  mode: AccessModeSchema,
48
48
  allowedSenders: z.union([z.literal('*'), z.array(z.string())]),
49
+ triggerMode: TriggerModeSchema.default('off'),
49
50
  proactive: z.boolean().default(false),
50
51
  });
51
52
  const DmEntrySchema = z.object({
52
53
  number: z.string(),
53
54
  mode: AccessModeSchema,
55
+ triggerMode: TriggerModeSchema.default('off'),
54
56
  proactive: z.boolean().default(false),
55
57
  });
56
58
  const AccessSchema = z
@@ -252,15 +254,22 @@ export function getLimitsForUser(senderNumber, isGroup) {
252
254
  isOwner: false,
253
255
  };
254
256
  }
255
- const DROP = { store: false, respond: false, reason: 'drop' };
257
+ const DROP = {
258
+ store: false,
259
+ respond: false,
260
+ triggerMode: 'off',
261
+ reason: 'drop',
262
+ };
256
263
  const storeOnly = (reason) => ({
257
264
  store: true,
258
265
  respond: false,
266
+ triggerMode: 'off',
259
267
  reason,
260
268
  });
261
- const storeAndRespond = (reason) => ({
269
+ const storeAndRespond = (reason, triggerMode = 'off') => ({
262
270
  store: true,
263
271
  respond: true,
272
+ triggerMode,
264
273
  reason,
265
274
  });
266
275
  export function checkAccess(params) {
@@ -274,12 +283,14 @@ export function checkAccess(params) {
274
283
  return DROP;
275
284
  if (group.mode === 'silent')
276
285
  return storeOnly('group silent');
277
- if (ownerAllowed)
278
- return storeAndRespond('owner fromMe in group');
279
- if (group.allowedSenders === '*')
280
- return storeAndRespond('group wildcard');
286
+ if (ownerAllowed) {
287
+ return storeAndRespond('owner fromMe in group', group.triggerMode);
288
+ }
289
+ if (group.allowedSenders === '*') {
290
+ return storeAndRespond('group wildcard', group.triggerMode);
291
+ }
281
292
  if (group.allowedSenders.includes(senderNumber)) {
282
- return storeAndRespond('group sender allowed');
293
+ return storeAndRespond('group sender allowed', group.triggerMode);
283
294
  }
284
295
  return storeOnly('group sender not in allowedSenders');
285
296
  }
@@ -302,7 +313,7 @@ export function checkAccess(params) {
302
313
  return DROP;
303
314
  if (mode === 'silent')
304
315
  return storeOnly('dm silent');
305
- return storeAndRespond('dm active');
316
+ return storeAndRespond('dm active', dmEntry?.triggerMode ?? 'off');
306
317
  }
307
318
  export async function discoverAddressGroupIfNew(params) {
308
319
  const parsed = parseAddress(params.address);
@@ -315,6 +326,7 @@ export async function discoverAddressGroupIfNew(params) {
315
326
  name: params.name || 'Unknown group',
316
327
  mode: 'off',
317
328
  allowedSenders: params.ownerSender ? [params.ownerSender] : [],
329
+ triggerMode: 'off',
318
330
  proactive: false,
319
331
  };
320
332
  save({ ...current, groups: [...current.groups, entry] });
@@ -339,6 +351,7 @@ export async function discoverGroupIfNew(sock, jid) {
339
351
  name,
340
352
  mode: 'off',
341
353
  allowedSenders: config.owner.number ? [config.owner.number] : [],
354
+ triggerMode: 'off',
342
355
  proactive: false,
343
356
  };
344
357
  save({ ...current, groups: [...current.groups, entry] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "WhatsApp and Telegram AI bot powered by Claude, Codex, or Grok with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",