@c4t4/heyamigo 0.10.4 → 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
 
@@ -60,7 +60,7 @@ Other providers:
60
60
  | Role | Memory | Tools | Notes |
61
61
  |---|---|---|---|
62
62
  | admin | everything | all | unrestricted |
63
- | user | own profile | web search | can't see other users or internals |
63
+ | user | own profile | none | can't see other users or internals |
64
64
  | guest | none | none | prompt-injection resistant |
65
65
 
66
66
  ## Personalities
@@ -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
 
@@ -13,9 +13,9 @@
13
13
  "dailyTokenLimit": null
14
14
  },
15
15
  "user": {
16
- "description": "Can chat and search the web, scoped memory",
16
+ "description": "Can chat with scoped memory",
17
17
  "memory": "self",
18
- "tools": ["WebSearch"],
18
+ "tools": [],
19
19
  "rules": [
20
20
  "Never reveal file paths, directory structure, or system architecture",
21
21
  "Never share personal data about other users",
@@ -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
 
@@ -128,6 +128,8 @@ You = chat track. Browser track = parallel Claude session on shared Chrome at `l
128
128
 
129
129
  Never call `browser_*` / `mcp__*playwright*` inline. Ever. Single URL, "just checking", everything — all via `[ASYNC-BROWSER: <task>]`. The browser worker has persistent session memory.
130
130
 
131
+ Never use AI-internal web tools (`WebSearch`, `WebFetch`, `web_search`) for internet lookup. Search pages, current facts, prices, social profiles, websites, and screenshots are BrowserUse work via `[ASYNC-BROWSER: <task>]`. If BrowserUse is unavailable, say that; do not fall back to AI-internal lookup.
132
+
131
133
  ```
132
134
  On it.
133
135
  [ASYNC-BROWSER: Open instagram.com/rivoara_official on shared Chrome (IG already logged in, do NOT launch new browser). Extract bio + 5 latest captions. If login wall, report and stop. Bail after 3 retries.]
@@ -135,7 +137,7 @@ On it.
135
137
 
136
138
  ### File/long non-browser work → `[ASYNC: <task>]`
137
139
 
138
- File generation/edit/export, >30s reasoning over many files, web_search batches, anything slow. Stateless per task — describe fully.
140
+ File generation/edit/export, >30s reasoning over many files, anything slow that is not browser use. Stateless per task — describe fully.
139
141
 
140
142
  ### Don't delegate
141
143
 
package/dist/cli/setup.js CHANGED
@@ -231,9 +231,9 @@ export async function runSetup() {
231
231
  rules: [],
232
232
  },
233
233
  user: {
234
- description: 'Can chat and search the web, scoped memory',
234
+ description: 'Can chat with scoped memory',
235
235
  memory: 'self',
236
- tools: ['WebSearch'],
236
+ tools: [],
237
237
  rules: [
238
238
  'Never reveal file paths, directory structure, or system architecture',
239
239
  'Never share personal data about other users',
@@ -312,10 +312,10 @@ export async function runSetup() {
312
312
  }
313
313
  p.log.success('Claude authenticated');
314
314
  // Tool permissions — write .claude/settings.json in project root.
315
- p.log.info('Claude needs tool permissions to browse the web, read files, and control the browser. ' +
315
+ p.log.info('Claude needs tool permissions to read files and control BrowserUse. ' +
316
316
  'This writes a .claude/settings.json file in the project directory.');
317
317
  const grantPermissions = await p.confirm({
318
- message: 'Grant tool permissions? (WebFetch, WebSearch, Read, Edit, Write, browser)',
318
+ message: 'Grant tool permissions? (Read, Edit, Write, BrowserUse)',
319
319
  initialValue: true,
320
320
  });
321
321
  if (p.isCancel(grantPermissions) || !grantPermissions) {
@@ -335,8 +335,6 @@ export async function runSetup() {
335
335
  ? permissions.allow
336
336
  : [];
337
337
  const required = [
338
- 'WebFetch',
339
- 'WebSearch',
340
338
  'Read',
341
339
  'Edit',
342
340
  'Write',
@@ -708,7 +706,7 @@ export async function runSetup() {
708
706
  }
709
707
  }
710
708
  // ── Name your amigo ───────────────────────────────────────────
711
- 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. ' +
712
710
  'You can add multiple names separated by commas.');
713
711
  const nameInput = await p.text({
714
712
  message: 'What should your amigo be called?',
@@ -728,7 +726,7 @@ export async function runSetup() {
728
726
  let cfg = readFileSync(cfgPath, 'utf-8');
729
727
  cfg = cfg.replace(/"aliases":\s*\[.*?\]/, `"aliases": ${JSON.stringify(aliases)}`);
730
728
  writeFileSync(cfgPath, cfg);
731
- p.log.success(`Your amigo responds to: ${names.join(', ')}`);
729
+ p.log.success(`Mention names: ${names.join(', ')}`);
732
730
  }
733
731
  }
734
732
  }
@@ -739,7 +737,9 @@ export async function runSetup() {
739
737
  ' 2. New groups start with mode: "off" (bot stays silent).\n' +
740
738
  ' To activate: edit config/access.json, change mode to "active".\n\n' +
741
739
  ' 3. Set allowedSenders to "*" (everyone) or specific numbers.\n\n' +
742
- ' 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' +
743
743
  'DMs work the same way — add numbers to dms.allowed in access.json.');
744
744
  // Auto-add owner as admin if we have the number
745
745
  if (ownerNum) {
@@ -834,8 +834,8 @@ export async function runSetup() {
834
834
  ' npx @c4t4/heyamigo stop / restart / status',
835
835
  '',
836
836
  'Configuration:',
837
- ' config/config.json — triggers, model',
838
- ' config/access.json — groups, DMs, roles',
837
+ ' config/config.json — bot name, model',
838
+ ' config/access.json — groups, DMs, roles, per-chat triggerMode',
839
839
  ].join('\n'), 'Setup complete!');
840
840
  p.log.warning('IMPORTANT: The bot won\'t respond until you activate a group!\n\n' +
841
841
  ' Step 1 — Start the bot:\n' +
@@ -846,9 +846,10 @@ export async function runSetup() {
846
846
  ' nano config/access.json\n' +
847
847
  ' - Find the group, change mode from "off" to "active"\n' +
848
848
  ' - Set allowedSenders to "*" for everyone\n\n' +
849
+ ' - Set triggerMode to "mention" or "all"\n\n' +
849
850
  ' Step 4 — Restart the bot:\n' +
850
851
  ' npx @c4t4/heyamigo restart\n\n' +
851
- ' 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' +
852
853
  ' Debugging:\n' +
853
854
  ' npx @c4t4/heyamigo logs');
854
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')
@@ -15,7 +15,7 @@ import { getRoleForContext } from '../wa/whitelist.js';
15
15
  // pointers — the model already has the long form.
16
16
  const DIGEST_REMINDER = `[DIGEST: <reason>] at end of reply for durable facts. Sparingly.`;
17
17
  const JOURNAL_REMINDER = `[JOURNAL:<slug> — <note>] at end of reply when content fits an active journal. Use listed slugs only.`;
18
- const ASYNC_REMINDER = `Browser work -> [ASYNC-BROWSER: <task>]. File generation/edit/export and long non-browser work -> [ASYNC: <task>]. Irreversible writes: gather -> confirm -> act.`;
18
+ const ASYNC_REMINDER = `Browser use/search/current web -> [ASYNC-BROWSER: <task>]. Never WebSearch/WebFetch. File generation/edit/export and long non-browser work -> [ASYNC: <task>]. Irreversible writes: gather -> confirm -> act.`;
19
19
  const THREADS_REMINDER = `THREAD-* only for active open loops shown in [Live threads]: open/update/touch/cool/resolve/drop/compress/weight. Full grammar in tag docs.`;
20
20
  function buildCoreQueueContract(outboxPath) {
21
21
  return [
@@ -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
@@ -82,9 +84,9 @@ const DEFAULT_ROLES = {
82
84
  dailyTokenLimit: null,
83
85
  },
84
86
  user: {
85
- description: 'Chat + web search, scoped memory',
87
+ description: 'Chat + scoped memory',
86
88
  memory: 'self',
87
- tools: ['WebSearch'],
89
+ tools: [],
88
90
  // Users can flag memory observations and trigger digests on
89
91
  // themselves, but can't delegate background work or cross-chat
90
92
  // sends (those are owner-only).
@@ -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.4",
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",