@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 +1 -1
- package/config/access.example.json +7 -5
- package/config/config.example.json +5 -3
- package/config/personalities/sharp.md +17 -1
- package/dist/cli/setup.js +3 -3
- package/dist/config.js +2 -0
- package/dist/gateway/bootstrap.js +13 -0
- package/dist/gateway/incoming.js +10 -4
- package/dist/memory/preamble.js +21 -1
- package/dist/wa/whitelist.js +18 -0
- package/package.json +1 -1
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-
|
|
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-
|
|
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.
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
+
}
|
package/dist/gateway/incoming.js
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 = {
|
package/dist/memory/preamble.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/wa/whitelist.js
CHANGED
|
@@ -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');
|