digital-brain 1.1.18 → 1.1.23

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.
@@ -163,11 +163,12 @@ Guardrails:
163
163
  - with `--provider codex-app`, requires a Codex desktop bridge automation/thread that writes response files
164
164
  - requires `--allow "Name"` or `--contact "+15551234567"` unless `--allow-all` is explicitly passed
165
165
  - single-threads reply generation so multiple incoming chats do not trigger overlapping sends
166
+ - debounces live messages per chat, default 4000ms, so message bursts get one combined reply; override with `--reply-debounce-ms <ms>`
166
167
  - skips likely business, notification, OTP, and service chats unless `--include-businesses` is passed or the chat is explicitly allowlisted by name or contact number
167
168
  - processes unread chats on startup unless `--no-process-unread` is passed
168
169
  - skips groups unless `--include-groups` is passed
169
170
  - uses a per-chat cooldown, default 20 minutes
170
- - caps replies per chat per run, default 5
171
+ - no reply cap by default, so conversations can continue naturally; pass `--max-replies-per-chat <n>` to add one
171
172
  - logs metadata by default, not full sent text
172
173
  - enforces the AI disclosure rule after repeated AI-assisted sends, but does not repeat it after that chat has already received a disclosure
173
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "digital-brain",
3
- "version": "1.1.18",
3
+ "version": "1.1.23",
4
4
  "description": "Your private digital imprint for AI assistants.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,8 +39,9 @@ const includeBusinesses = Boolean(args["include-businesses"]);
39
39
  const sendEnabled = Boolean(args.yes) || config.outboundMode === "auto-send";
40
40
  const processUnreadOnStart = !Boolean(args["no-process-unread"]);
41
41
  const cooldownMinutes = numberArg("cooldown-minutes", 20);
42
- const maxRepliesPerChat = numberArg("max-replies-per-chat", 5);
42
+ const maxRepliesPerChat = numberArg("max-replies-per-chat", 0);
43
43
  const maxContextChars = numberArg("max-context-chars", 12000);
44
+ const replyDebounceMs = numberArg("reply-debounce-ms", 4000);
44
45
  const outboundLogMode = args["log-mode"] || config.outboundLogMode || "metadata";
45
46
  const state = loadState();
46
47
  const whitelist = loadWhitelist();
@@ -48,6 +49,7 @@ const hasStoredScope = Object.keys(whitelist.allowedChats || {}).length > 0;
48
49
  const hasInitialScope = allowAll || allow.length > 0 || contactNumbers.length > 0 || hasStoredScope;
49
50
  const interactiveTerminal = Boolean(input.isTTY && output.isTTY);
50
51
  let queueTail = Promise.resolve();
52
+ const pendingLiveReplies = new Map();
51
53
  const rl = interactiveTerminal ? readline.createInterface({ input, output }) : null;
52
54
 
53
55
  fs.mkdirSync(outboundDir, { recursive: true });
@@ -88,6 +90,8 @@ client.on("ready", async () => {
88
90
  if (provider === "codex") console.log(`Codex command: ${codexCommand}`);
89
91
  if (provider === "codex-app") console.log(`Codex App bridge: ${codexAppBridgeDir}`);
90
92
  if (!includeBusinesses) console.log("Likely business, notification, OTP, and service chats are skipped by default.");
93
+ console.log(maxRepliesPerChat > 0 ? `Reply cap: ${maxRepliesPerChat} per chat per run.` : "Reply cap: unlimited.");
94
+ console.log(`Live reply debounce: ${replyDebounceMs}ms.`);
91
95
  try {
92
96
  if (processUnreadOnStart) {
93
97
  await processUnreadChats();
@@ -103,7 +107,7 @@ client.on("ready", async () => {
103
107
  client.on("message", async (message) => {
104
108
  try {
105
109
  console.log(`Received WhatsApp message event: ${summarize(message.body || "[non-text message]")}`);
106
- enqueueMessage(message);
110
+ await scheduleLiveMessage(message);
107
111
  } catch (error) {
108
112
  console.error(`Auto-reply error: ${error.message}`);
109
113
  }
@@ -129,7 +133,7 @@ async function processUnreadChats() {
129
133
  console.log(`Skipping unread chat during cooldown: ${name}`);
130
134
  continue;
131
135
  }
132
- if (replyCount(name) >= maxRepliesPerChat) {
136
+ if (isAtReplyCap(name)) {
133
137
  console.log(`Skipping unread chat at reply cap: ${name}`);
134
138
  continue;
135
139
  }
@@ -189,6 +193,20 @@ function enqueueMessage(message, knownChat = null) {
189
193
  return queueTail;
190
194
  }
191
195
 
196
+ async function scheduleLiveMessage(message) {
197
+ if (message.fromMe || message.isStatus) return;
198
+ const chat = await message.getChat();
199
+ const key = chatKey(chat);
200
+ const existing = pendingLiveReplies.get(key);
201
+ if (existing?.timer) clearTimeout(existing.timer);
202
+ const timer = setTimeout(() => {
203
+ pendingLiveReplies.delete(key);
204
+ enqueueMessage(message, chat);
205
+ }, replyDebounceMs);
206
+ pendingLiveReplies.set(key, { message, chat, timer });
207
+ console.log(`Queued debounced reply for ${chatName(chat)} in ${replyDebounceMs}ms.`);
208
+ }
209
+
192
210
  async function ensureChatApproved(chat, recentMessages) {
193
211
  const name = chatName(chat);
194
212
  if (isExplicitlyAllowed(chat) || isChatWhitelisted(chat)) return true;
@@ -245,7 +263,7 @@ async function handleMessage(message, knownChat = null) {
245
263
  console.log(`Skipping chat during cooldown: ${name}`);
246
264
  return;
247
265
  }
248
- if (replyCount(name) >= maxRepliesPerChat) {
266
+ if (isAtReplyCap(name)) {
249
267
  console.log(`Skipping chat at reply cap: ${name}`);
250
268
  return;
251
269
  }
@@ -273,7 +291,7 @@ async function handleMessage(message, knownChat = null) {
273
291
  const reply = await generateReply(prompt);
274
292
  console.log(`Generated reply for ${name} in ${Date.now() - startedAt}ms: ${summarize(reply || "[empty reply]")}`);
275
293
  const finalReply = disclosure.required && !containsDisclosure(reply)
276
- ? `Just flagging this is my AI assistant helping draft/send this. ${reply}`
294
+ ? `btw ai is helping me reply rn. ${reply}`
277
295
  : reply;
278
296
 
279
297
  if (!finalReply.trim()) return;
@@ -298,16 +316,23 @@ function buildPrompt({ chatName, incomingBody, recentMessages, disclosureRequire
298
316
  return [
299
317
  "You are helping the user reply on WhatsApp.",
300
318
  "Write exactly one message to send as the user.",
301
- "Be natural, concise, and relationship-appropriate.",
319
+ "Be natural, terse, and relationship-appropriate.",
320
+ "Default to 1-12 words for casual chats unless the incoming message clearly requires detail.",
302
321
  "First infer the immediate intent of the current conversation from the recent chat. Continue that thread only.",
322
+ "If the other person sent multiple messages in a row, answer the combined latest intent once.",
323
+ "Do not repeat facts, plans, suggestions, or context that were already stated in the recent chat unless confirming them briefly.",
324
+ "If the chat includes a URL, do not pretend you opened or inspected it. React only to what the sender said, or ask if the user should check it.",
303
325
  "Do not start with hey/hi unless the recent chat itself uses that greeting pattern.",
304
326
  "Mirror the vocabulary, register, and pacing already present in this chat.",
305
327
  "Match the user's own communication style from My Communication Style. If it says lowercase-heavy or undercapitalized, prefer lowercase casual texting.",
306
328
  "Use lexical signals from My Communication Style: recurring style words, openers, short phrase shapes, punctuation habits, and lowercase-i behavior.",
329
+ "Do not overuse bro, lol, haha, emojis, or question marks. Only use them if the recent chat clearly does.",
330
+ "Avoid assistant-like niceness and filler such as sounds perfect, happy to, sure thing, smooth, quick, no worries, no demon stuff, digital prep chef, digital neil, spitting facts, living in the future, or let’s unless that exact energy is already in the chat.",
331
+ "If the recipient asks about AI, answer directly in the user's casual tone and do not overexplain.",
307
332
  "Do not sound like customer support, corporate email, or a generic AI assistant.",
308
333
  "Use the user's local memory context, but do not reveal private notes or say you read a vault.",
309
334
  "Do not invent facts, commitments, times, or promises.",
310
- disclosureRequired ? "This send requires AI disclosure. Include a short clear disclosure in the message." : "Do not mention AI unless disclosure is required.",
335
+ disclosureRequired ? "This send requires AI disclosure. Use a short casual disclosure like: btw ai is helping me reply rn. Do not make it cute or apologetic." : "Do not mention AI unless the incoming message asks about it.",
311
336
  "",
312
337
  `Chat: ${chatName}`,
313
338
  "",
@@ -370,8 +395,8 @@ async function generateOpenAiReply(prompt) {
370
395
  body: JSON.stringify({
371
396
  model,
372
397
  input: prompt,
373
- max_output_tokens: 160,
374
- temperature: 0.35,
398
+ max_output_tokens: 80,
399
+ temperature: 0.25,
375
400
  }),
376
401
  });
377
402
  } catch (error) {
@@ -686,6 +711,10 @@ function replyCount(name) {
686
711
  return state.sentCountByChat[name] || 0;
687
712
  }
688
713
 
714
+ function isAtReplyCap(name) {
715
+ return maxRepliesPerChat > 0 && replyCount(name) >= maxRepliesPerChat;
716
+ }
717
+
689
718
  function markProcessed(message, name, options = { sent: true }) {
690
719
  state.processedMessageIds.push(message.id?._serialized);
691
720
  state.processedMessageIds = state.processedMessageIds.filter(Boolean).slice(-1000);