@c4t4/heyamigo 0.1.15 → 0.1.16
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/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": {
|
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');
|