digital-brain 0.1.0

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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/bin/digital-brain.js +209 -0
  4. package/docs/AUTOMATIONS.md +70 -0
  5. package/docs/PRIVACY.md +17 -0
  6. package/docs/ROADMAP.md +9 -0
  7. package/examples/sample-vault/04 People/Interpreted Relationships/Close Friend.md +29 -0
  8. package/examples/sample-vault/04 People/Interpreted Relationships/Mom.md +30 -0
  9. package/examples/sample-vault/04 People/Interpreted Relationships/Project Team.md +30 -0
  10. package/examples/sample-vault/06 AI Memory/Interpreted Relationship Memory.md +7 -0
  11. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Close Friend.md +29 -0
  12. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Mom.md +30 -0
  13. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Project Team.md +30 -0
  14. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Relationship Map.md +29 -0
  15. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/interpreted_relationship_models.json +109 -0
  16. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/relationship_profiles.json +68 -0
  17. package/examples/sample-vault/08 Sources/WhatsApp/Raw/2026-01-01.jsonl +6 -0
  18. package/examples/sample-vault/08 Sources/WhatsApp/relationship_overrides.json +7 -0
  19. package/lib/fs.js +32 -0
  20. package/package.json +43 -0
  21. package/scripts/digital_brain_relationship_extractor.py +165 -0
  22. package/scripts/digital_brain_relationship_interpreter.py +196 -0
  23. package/scripts/digital_brain_whatsapp_mac_sync.py +153 -0
  24. package/templates/vault/00 Home/How AI Should Use This Vault.md +11 -0
  25. package/templates/vault/00 Home/Start Here.md +11 -0
  26. package/templates/vault/01 Identity/Working Profile.md +11 -0
  27. package/templates/vault/04 People/Relationship Overrides.md +16 -0
  28. package/templates/vault/06 AI Memory/Interpreted Relationship Memory.md +4 -0
  29. package/templates/vault/06 AI Memory/What AI Should Remember.md +4 -0
  30. package/templates/vault/08 Sources/WhatsApp/Outbound/README.md +20 -0
  31. package/templates/vault/08 Sources/WhatsApp/relationship_overrides.example.json +8 -0
  32. package/templates/vault/AGENTS.md +19 -0
  33. package/templates/vault/CLAUDE.md +6 -0
  34. package/templates/vault/GEMINI.md +5 -0
  35. package/whatsapp-web/send.mjs +139 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import sqlite3
5
+ import time
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ CORE_DATA_EPOCH_OFFSET = 978_307_200
10
+ DEFAULT_DB = Path.home() / "Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite"
11
+
12
+
13
+ def main():
14
+ args = parse_args()
15
+ vault = args.vault.resolve()
16
+ whats_app = vault / "08 Sources" / "WhatsApp"
17
+ raw_dir = whats_app / "Raw"
18
+ chats_dir = whats_app / "Chats"
19
+ state_dir = whats_app / ".sync-state"
20
+ seen_path = state_dir / "mac-seen-message-ids.json"
21
+ for directory in (raw_dir, chats_dir, state_dir):
22
+ directory.mkdir(parents=True, exist_ok=True)
23
+
24
+ seen = load_seen(seen_path)
25
+ total = 0
26
+ while True:
27
+ added = sync_once(args, seen, raw_dir, chats_dir, whats_app)
28
+ total += added
29
+ save_seen(seen_path, seen)
30
+ print(f"Added {added} new messages. Total this run: {total}.")
31
+ if not args.live:
32
+ break
33
+ time.sleep(args.interval)
34
+
35
+
36
+ def sync_once(args, seen, raw_dir, chats_dir, whats_app):
37
+ if not args.db.exists():
38
+ raise SystemExit(f"WhatsApp database not found: {args.db}")
39
+
40
+ cutoff = time.time() - args.days * 24 * 60 * 60 - CORE_DATA_EPOCH_OFFSET
41
+ conn = sqlite3.connect(f"file:{args.db}?mode=ro", uri=True)
42
+ conn.row_factory = sqlite3.Row
43
+ rows = conn.execute(
44
+ """
45
+ SELECT m.Z_PK message_pk, m.ZSTANZAID stanza_id, m.ZMESSAGEDATE message_date,
46
+ m.ZISFROMME is_from_me, m.ZFROMJID from_jid, m.ZTOJID to_jid,
47
+ m.ZPUSHNAME push_name, m.ZTEXT text, m.ZMESSAGETYPE message_type,
48
+ c.Z_PK chat_pk, c.ZPARTNERNAME chat_name, c.ZCONTACTJID chat_jid,
49
+ c.ZSESSIONTYPE session_type
50
+ FROM ZWAMESSAGE m
51
+ LEFT JOIN ZWACHATSESSION c ON c.Z_PK = m.ZCHATSESSION
52
+ WHERE m.ZMESSAGEDATE >= ?
53
+ AND m.ZTEXT IS NOT NULL
54
+ AND length(m.ZTEXT) > 0
55
+ ORDER BY m.ZMESSAGEDATE ASC, m.Z_PK ASC
56
+ """,
57
+ (cutoff,),
58
+ ).fetchall()
59
+ conn.close()
60
+
61
+ added = 0
62
+ for row in rows:
63
+ record = row_to_record(row, args.self_name)
64
+ if args.no_groups and record["isGroup"]:
65
+ continue
66
+ if args.chat and args.chat.lower() not in record["chatName"].lower():
67
+ continue
68
+ if record["id"] in seen:
69
+ continue
70
+ append_jsonl(raw_dir, record)
71
+ append_markdown(chats_dir, whats_app, record, args.markdown_mode)
72
+ seen.add(record["id"])
73
+ added += 1
74
+ return added
75
+
76
+
77
+ def row_to_record(row, self_name):
78
+ timestamp = datetime.fromtimestamp(float(row["message_date"]) + CORE_DATA_EPOCH_OFFSET, tz=timezone.utc).isoformat()
79
+ chat_name = row["chat_name"] or row["chat_jid"] or "Unknown Chat"
80
+ from_me = bool(row["is_from_me"])
81
+ return {
82
+ "id": str(row["stanza_id"] or f"mac-db-{row['message_pk']}"),
83
+ "source": "WhatsApp Mac app ChatStorage.sqlite",
84
+ "timestamp": timestamp,
85
+ "chatPk": row["chat_pk"],
86
+ "chatName": chat_name,
87
+ "chatJid": row["chat_jid"],
88
+ "isGroup": bool(row["chat_jid"] and "@g.us" in row["chat_jid"]) or row["session_type"] not in (None, 0),
89
+ "fromMe": from_me,
90
+ "author": self_name if from_me else (row["push_name"] or row["from_jid"] or "Unknown"),
91
+ "fromJid": row["from_jid"],
92
+ "toJid": row["to_jid"],
93
+ "messageType": row["message_type"],
94
+ "body": row["text"] or "",
95
+ }
96
+
97
+
98
+ def append_jsonl(raw_dir, record):
99
+ with (raw_dir / f"{record['timestamp'][:10]}.jsonl").open("a", encoding="utf-8") as f:
100
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
101
+
102
+
103
+ def append_markdown(chats_dir, whats_app, record, mode):
104
+ if mode == "none":
105
+ return
106
+ if mode == "month":
107
+ directory = whats_app / "ChatsByMonth" / record["timestamp"][:7]
108
+ directory.mkdir(parents=True, exist_ok=True)
109
+ file_path = directory / f"{safe_filename(record['chatName'])}.md"
110
+ else:
111
+ file_path = chats_dir / f"{safe_filename(record['chatName'])}.md"
112
+ if not file_path.exists():
113
+ file_path.write_text(f"# {record['chatName']}\n\nSynced from WhatsApp Mac app.\n\n", encoding="utf-8")
114
+ speaker = record["author"]
115
+ body = " ".join(record["body"].split())
116
+ with file_path.open("a", encoding="utf-8") as f:
117
+ f.write(f"- {record['timestamp']} | {speaker}: {body}\n")
118
+
119
+
120
+ def load_seen(path):
121
+ if not path.exists():
122
+ return set()
123
+ try:
124
+ return set(json.loads(path.read_text(encoding="utf-8")))
125
+ except Exception:
126
+ return set()
127
+
128
+
129
+ def save_seen(path, seen):
130
+ path.write_text(json.dumps(sorted(seen), indent=2), encoding="utf-8")
131
+
132
+
133
+ def safe_filename(value):
134
+ cleaned = "".join("-" if char in '/:\\?%*"<>|' else char for char in value)
135
+ return (" ".join(cleaned.split()).strip() or "Unknown Chat")[:120]
136
+
137
+
138
+ def parse_args():
139
+ parser = argparse.ArgumentParser()
140
+ parser.add_argument("--vault", type=Path, required=True)
141
+ parser.add_argument("--days", type=int, default=30)
142
+ parser.add_argument("--live", action="store_true")
143
+ parser.add_argument("--interval", type=int, default=60)
144
+ parser.add_argument("--chat", default="")
145
+ parser.add_argument("--no-groups", action="store_true")
146
+ parser.add_argument("--db", type=Path, default=DEFAULT_DB)
147
+ parser.add_argument("--self-name", default="Me")
148
+ parser.add_argument("--markdown-mode", choices=["chat", "month", "none"], default="chat")
149
+ return parser.parse_args()
150
+
151
+
152
+ if __name__ == "__main__":
153
+ main()
@@ -0,0 +1,11 @@
1
+ # How AI Should Use This Vault
2
+
3
+ Use this vault to understand the user's preferences, relationships, communication patterns, and working context.
4
+
5
+ Rules:
6
+
7
+ - Prefer local evidence over assumptions.
8
+ - Be clear when a claim is inferred.
9
+ - Treat generated relationship notes as provisional.
10
+ - Do not send messages without explicit permission.
11
+
@@ -0,0 +1,11 @@
1
+ # Start Here
2
+
3
+ Digital Brain is your local context layer for AI assistants.
4
+
5
+ Core notes:
6
+
7
+ - [[How AI Should Use This Vault]]
8
+ - [[Working Profile]]
9
+ - [[What AI Should Remember]]
10
+ - [[Interpreted Relationship Memory]]
11
+
@@ -0,0 +1,11 @@
1
+ # Working Profile
2
+
3
+ Fill this in manually or let Digital Brain update it over time.
4
+
5
+ - Name:
6
+ - Location:
7
+ - Work:
8
+ - Current focus:
9
+ - Communication style:
10
+ - Important context:
11
+
@@ -0,0 +1,16 @@
1
+ # Relationship Overrides
2
+
3
+ Use `08 Sources/WhatsApp/relationship_overrides.json` to manually label important people.
4
+
5
+ Example:
6
+
7
+ ```json
8
+ {
9
+ "Mom": {
10
+ "role": "mother",
11
+ "confidence": "high",
12
+ "notes": "Manual label."
13
+ }
14
+ }
15
+ ```
16
+
@@ -0,0 +1,4 @@
1
+ # Interpreted Relationship Memory
2
+
3
+ Run `digital-brain interpret` to populate this file.
4
+
@@ -0,0 +1,4 @@
1
+ # What AI Should Remember
2
+
3
+ Add durable user preferences here.
4
+
@@ -0,0 +1,20 @@
1
+ # WhatsApp Outbound
2
+
3
+ Outbound sending is disabled unless you explicitly run the sender with `--yes`.
4
+
5
+ Draft:
6
+
7
+ ```bash
8
+ digital-brain send-whatsapp --vault . --to "Name" --message "Text"
9
+ ```
10
+
11
+ Send:
12
+
13
+ ```bash
14
+ digital-brain send-whatsapp --vault . --to "Name" --message "Text" --yes
15
+ ```
16
+
17
+ Disclosure rule:
18
+
19
+ - If Digital Brain sends multiple AI-assisted messages in the same chat, it must disclose that AI is helping after roughly 2 messages.
20
+ - The sender enforces this on the third AI-assisted send within 24 hours unless the message already includes a disclosure.
@@ -0,0 +1,8 @@
1
+ {
2
+ "Mom": {
3
+ "role": "mother",
4
+ "confidence": "high",
5
+ "notes": "Example only. Copy to relationship_overrides.json and edit."
6
+ }
7
+ }
8
+
@@ -0,0 +1,19 @@
1
+ # Digital Brain Context
2
+
3
+ This is a Digital Brain vault.
4
+
5
+ Before answering personal-context questions, read:
6
+
7
+ - `00 Home/Start Here.md`
8
+ - `00 Home/How AI Should Use This Vault.md`
9
+ - `01 Identity/Working Profile.md`
10
+ - `06 AI Memory/What AI Should Remember.md`
11
+ - `06 AI Memory/Interpreted Relationship Memory.md`
12
+
13
+ Rules:
14
+
15
+ - Keep answers direct.
16
+ - Separate facts from interpretation.
17
+ - Treat relationship labels as editable working notes.
18
+ - Do not expose private summaries unless asked.
19
+
@@ -0,0 +1,6 @@
1
+ # Digital Brain Context
2
+
3
+ This is a Digital Brain vault. Use it as local personal context when relevant.
4
+
5
+ Start with `00 Home/Start Here.md`.
6
+
@@ -0,0 +1,5 @@
1
+ # Digital Brain Context
2
+
3
+ This is a Digital Brain vault. Use it as local personal context when relevant.
4
+
5
+ Start with `00 Home/Start Here.md`.
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import qrcode from "qrcode-terminal";
4
+ import pkg from "whatsapp-web.js";
5
+
6
+ const { Client, LocalAuth } = pkg;
7
+ const args = parseArgs(process.argv.slice(2));
8
+
9
+ if (!args.vault || !args.to || !args.message) usage();
10
+
11
+ const vault = path.resolve(args.vault);
12
+ const whatsAppDir = path.join(vault, "08 Sources", "WhatsApp");
13
+ const outboundDir = path.join(whatsAppDir, "Outbound");
14
+ const sessionDir = path.join(whatsAppDir, ".session");
15
+ fs.mkdirSync(outboundDir, { recursive: true });
16
+
17
+ if (!args.yes) {
18
+ console.log("Draft only. Nothing sent.");
19
+ console.log(`To: ${args.to}`);
20
+ console.log(`Message: ${args.message}`);
21
+ console.log("Re-run with --yes to send.");
22
+ process.exit(2);
23
+ }
24
+
25
+ const client = new Client({
26
+ authStrategy: new LocalAuth({ clientId: "digital-brain", dataPath: sessionDir }),
27
+ puppeteer: { headless: false, args: ["--no-sandbox", "--disable-setuid-sandbox"] },
28
+ });
29
+
30
+ client.on("qr", (qr) => {
31
+ console.log("Scan this QR in WhatsApp > Linked devices:");
32
+ qrcode.generate(qr, { small: true });
33
+ });
34
+
35
+ client.on("ready", async () => {
36
+ try {
37
+ const chat = await resolveChat(args.to);
38
+ const disclosure = disclosureStatus(chat);
39
+ if (disclosure.required && !containsDisclosure(args.message) && !args["skip-disclosure-check"]) {
40
+ throw new Error(
41
+ 'AI disclosure required before sending. Add a clear disclosure like: "Just flagging this is my AI assistant helping draft/send this."',
42
+ );
43
+ }
44
+ const sent = await chat.sendMessage(args.message);
45
+ const record = {
46
+ timestamp: new Date().toISOString(),
47
+ to: args.to,
48
+ resolvedChatName: chatName(chat),
49
+ message: args.message,
50
+ messageId: sent.id?._serialized || null,
51
+ aiAssisted: !args.human,
52
+ disclosureIncluded: containsDisclosure(args.message),
53
+ };
54
+ fs.appendFileSync(path.join(outboundDir, "sent.jsonl"), `${JSON.stringify(record)}\n`);
55
+ fs.appendFileSync(path.join(outboundDir, "Sent.md"), `- ${record.timestamp} | ${record.resolvedChatName}: ${record.message}\n`);
56
+ console.log(`Sent to ${record.resolvedChatName}`);
57
+ await client.destroy();
58
+ process.exit(0);
59
+ } catch (error) {
60
+ console.error(`Send failed: ${error.message}`);
61
+ await client.destroy();
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ client.initialize();
67
+
68
+ async function resolveChat(target) {
69
+ const number = normalizePhone(target);
70
+ if (number) {
71
+ const id = await client.getNumberId(number);
72
+ if (!id) throw new Error(`No WhatsApp account found for phone ${target}`);
73
+ return await client.getChatById(id._serialized);
74
+ }
75
+ const chats = await client.getChats();
76
+ const matches = chats.filter((chat) => chatName(chat).toLowerCase().includes(target.toLowerCase()));
77
+ if (matches.length === 0) throw new Error(`No chat matched "${target}"`);
78
+ if (matches.length > 1) throw new Error(`Multiple chats matched "${target}": ${matches.slice(0, 10).map(chatName).join(", ")}`);
79
+ return matches[0];
80
+ }
81
+
82
+ function normalizePhone(input) {
83
+ if (!input.startsWith("+") && !/^\d{8,}$/.test(input)) return "";
84
+ return input.replace(/[^\d]/g, "");
85
+ }
86
+
87
+ function chatName(chat) {
88
+ return chat.name || chat.formattedTitle || chat.id?._serialized || "Unknown Chat";
89
+ }
90
+
91
+ function parseArgs(argv) {
92
+ const out = { yes: false };
93
+ for (let i = 0; i < argv.length; i += 1) {
94
+ const arg = argv[i];
95
+ if (arg === "--yes") out.yes = true;
96
+ else if (arg === "--human") out.human = true;
97
+ else if (arg === "--skip-disclosure-check") out["skip-disclosure-check"] = true;
98
+ else if (arg.startsWith("--")) {
99
+ const key = arg.slice(2);
100
+ out[key] = argv[++i] || "";
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+
106
+ function usage() {
107
+ console.error('Usage: digital-brain send-whatsapp --vault <path> --to "Name" --message "Text" [--yes]');
108
+ process.exit(1);
109
+ }
110
+
111
+ function disclosureStatus(chat) {
112
+ const logPath = path.join(outboundDir, "sent.jsonl");
113
+ if (!fs.existsSync(logPath)) return { required: false, count: 0 };
114
+
115
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
116
+ const name = chatName(chat);
117
+ const count = fs
118
+ .readFileSync(logPath, "utf8")
119
+ .split("\n")
120
+ .filter(Boolean)
121
+ .map((line) => {
122
+ try {
123
+ return JSON.parse(line);
124
+ } catch {
125
+ return null;
126
+ }
127
+ })
128
+ .filter((record) => record)
129
+ .filter((record) => record.resolvedChatName === name)
130
+ .filter((record) => record.aiAssisted !== false)
131
+ .filter((record) => new Date(record.timestamp).getTime() >= cutoff)
132
+ .filter((record) => !record.disclosureIncluded).length;
133
+
134
+ return { required: count >= 2, count };
135
+ }
136
+
137
+ function containsDisclosure(message) {
138
+ return /\b(ai|assistant|automated|bot)\b/i.test(message);
139
+ }