@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 +2 -2
- package/config/access.example.json +14 -9
- package/config/config.example.json +0 -2
- package/config/memory-instructions.md +3 -1
- package/dist/cli/setup.js +13 -12
- package/dist/config.js +1 -3
- package/dist/gateway/ingest.js +1 -1
- package/dist/gateway/triggers.js +1 -4
- package/dist/memory/preamble.js +1 -1
- package/dist/wa/whitelist.js +24 -11
- package/package.json +1 -1
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
|
|
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 |
|
|
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
|
|
16
|
+
"description": "Can chat with scoped memory",
|
|
17
17
|
"memory": "self",
|
|
18
|
-
"tools": [
|
|
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
|
}
|
|
@@ -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,
|
|
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
|
|
234
|
+
description: 'Can chat with scoped memory',
|
|
235
235
|
memory: 'self',
|
|
236
|
-
tools: [
|
|
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
|
|
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? (
|
|
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.
|
|
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(`
|
|
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.
|
|
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 —
|
|
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 —
|
|
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({
|
package/dist/gateway/ingest.js
CHANGED
|
@@ -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
|
-
|
|
144
|
+
mode: decision.triggerMode,
|
|
145
145
|
text: stored.text,
|
|
146
146
|
mentionedBot: incoming.triggerHints?.mentionedBot,
|
|
147
147
|
replyToBot: incoming.triggerHints?.replyToBot,
|
package/dist/gateway/triggers.js
CHANGED
|
@@ -11,10 +11,7 @@ function aliasMatches(text, aliases) {
|
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
export function checkTrigger(params) {
|
|
14
|
-
const {
|
|
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')
|
package/dist/memory/preamble.js
CHANGED
|
@@ -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
|
|
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 [
|
package/dist/wa/whitelist.js
CHANGED
|
@@ -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 +
|
|
87
|
+
description: 'Chat + scoped memory',
|
|
86
88
|
memory: 'self',
|
|
87
|
-
tools: [
|
|
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 = {
|
|
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
|
-
|
|
280
|
-
|
|
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