digital-brain 0.1.7 → 1.0.3

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 (47) hide show
  1. package/README.md +20 -2
  2. package/bin/digital-brain.js +136 -20
  3. package/docs/INTEGRATIONS.md +72 -0
  4. package/docs/PRIVACY.md +3 -1
  5. package/docs/SETUP.md +33 -1
  6. package/examples/sample-vault/{04 People/Interpreted Relationships/Close Friend.md → 06 AI Memory/Generated Relationship Drafts/Close Friend (WhatsApp).md } +4 -3
  7. package/examples/sample-vault/{04 People/Interpreted Relationships/Mom.md → 06 AI Memory/Generated Relationship Drafts/Mom (WhatsApp).md } +4 -3
  8. package/examples/sample-vault/{08 Sources/WhatsApp/Analysis/Interpreted/Project Team.md → 06 AI Memory/Generated Relationship Drafts/Project Team (WhatsApp).md } +4 -3
  9. package/examples/sample-vault/06 AI Memory/Interpreted Relationship Memory.md +3 -3
  10. package/examples/sample-vault/06 AI Memory/Person Context Index.md +26 -0
  11. package/examples/sample-vault/06 AI Memory/Person Reply Context.md +26 -0
  12. package/examples/sample-vault/08 Sources/{WhatsApp/Analysis/Interpreted/Close Friend.md → Analysis/Interpreted/Close Friend (WhatsApp).md } +4 -3
  13. package/examples/sample-vault/08 Sources/{WhatsApp/Analysis/Interpreted/Mom.md → Analysis/Interpreted/Mom (WhatsApp).md } +4 -3
  14. package/examples/sample-vault/{04 People/Interpreted Relationships/Project Team.md → 08 Sources/Analysis/Interpreted/Project Team (WhatsApp).md } +4 -3
  15. package/examples/sample-vault/08 Sources/Analysis/Relationship Map.md +38 -0
  16. package/examples/sample-vault/08 Sources/Analysis/interpreted_relationship_models.json +175 -0
  17. package/examples/sample-vault/08 Sources/Analysis/person_identity_map.json +78 -0
  18. package/examples/sample-vault/08 Sources/Analysis/relationship_profiles.json +122 -0
  19. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Close Friend (WhatsApp).md +44 -0
  20. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Mom (WhatsApp).md +45 -0
  21. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Interpreted/Project Team (WhatsApp).md +45 -0
  22. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/Relationship Map.md +9 -3
  23. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/interpreted_relationship_models.json +18 -0
  24. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/person_identity_map.json +78 -0
  25. package/examples/sample-vault/08 Sources/WhatsApp/Analysis/relationship_profiles.json +18 -0
  26. package/examples/sample-vault/08 Sources/WhatsApp/Raw/2026-01-01.jsonl +6 -6
  27. package/lib/fs.js +7 -1
  28. package/package.json +2 -1
  29. package/scripts/digital_brain_imessage_sync.py +175 -0
  30. package/scripts/digital_brain_linkedin_export_import.py +214 -0
  31. package/scripts/digital_brain_relationship_extractor.py +189 -12
  32. package/scripts/digital_brain_relationship_interpreter.py +104 -15
  33. package/scripts/digital_brain_slack_export_import.py +181 -0
  34. package/scripts/digital_brain_whatsapp_mac_sync.py +37 -8
  35. package/templates/vault/00 Home/How AI Should Use This Vault.md +1 -1
  36. package/templates/vault/00 Home/Start Here.md +2 -1
  37. package/templates/vault/04 People/Relationship Overrides.md +2 -1
  38. package/templates/vault/06 AI Memory/Generated Relationship Drafts/README.md +5 -0
  39. package/templates/vault/06 AI Memory/Interpreted Relationship Memory.md +1 -2
  40. package/templates/vault/06 AI Memory/Person Context Index.md +4 -0
  41. package/templates/vault/06 AI Memory/Person Reply Context.md +4 -0
  42. package/templates/vault/08 Sources/README.md +5 -0
  43. package/templates/vault/08 Sources/WhatsApp/Outbound/README.md +2 -2
  44. package/templates/vault/AGENTS.md +5 -1
  45. package/templates/vault/CLAUDE.md +3 -0
  46. package/templates/vault/GEMINI.md +4 -0
  47. package/whatsapp-web/send.mjs +32 -5
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  import argparse
3
+ import hashlib
3
4
  import json
4
5
  import sqlite3
5
6
  import time
@@ -60,7 +61,7 @@ def sync_once(args, seen, raw_dir, chats_dir, whats_app):
60
61
 
61
62
  added = 0
62
63
  for row in rows:
63
- record = row_to_record(row, args.self_name)
64
+ record = row_to_record(row, args.self_name, args.privacy_mode)
64
65
  if args.no_groups and record["isGroup"]:
65
66
  continue
66
67
  if args.chat and args.chat.lower() not in record["chatName"].lower():
@@ -74,13 +75,16 @@ def sync_once(args, seen, raw_dir, chats_dir, whats_app):
74
75
  return added
75
76
 
76
77
 
77
- def row_to_record(row, self_name):
78
+ def row_to_record(row, self_name, privacy_mode):
78
79
  timestamp = datetime.fromtimestamp(float(row["message_date"]) + CORE_DATA_EPOCH_OFFSET, tz=timezone.utc).isoformat()
79
80
  chat_name = row["chat_name"] or row["chat_jid"] or "Unknown Chat"
80
81
  from_me = bool(row["is_from_me"])
82
+ body = row["text"] or ""
83
+ record_id = compound_id(row, timestamp)
81
84
  return {
82
- "id": str(row["stanza_id"] or f"mac-db-{row['message_pk']}"),
85
+ "id": record_id,
83
86
  "source": "WhatsApp Mac app ChatStorage.sqlite",
87
+ "sourceSystem": "WhatsApp",
84
88
  "timestamp": timestamp,
85
89
  "chatPk": row["chat_pk"],
86
90
  "chatName": chat_name,
@@ -91,10 +95,23 @@ def row_to_record(row, self_name):
91
95
  "fromJid": row["from_jid"],
92
96
  "toJid": row["to_jid"],
93
97
  "messageType": row["message_type"],
94
- "body": row["text"] or "",
98
+ "body": "" if privacy_mode == "metadata-only" else body,
99
+ "bodyHash": hashlib.sha256(body.encode("utf-8")).hexdigest() if privacy_mode == "metadata-only" else "",
100
+ "bodyCharCount": len(body),
95
101
  }
96
102
 
97
103
 
104
+ def compound_id(row, timestamp):
105
+ parts = [
106
+ "whatsapp",
107
+ str(row["chat_pk"] or row["chat_jid"] or "unknown-chat"),
108
+ str(row["stanza_id"] or "no-stanza"),
109
+ str(row["message_pk"] or "no-pk"),
110
+ timestamp,
111
+ ]
112
+ return "::".join(parts)
113
+
114
+
98
115
  def append_jsonl(raw_dir, record):
99
116
  with (raw_dir / f"{record['timestamp'][:10]}.jsonl").open("a", encoding="utf-8") as f:
100
117
  f.write(json.dumps(record, ensure_ascii=False) + "\n")
@@ -110,9 +127,9 @@ def append_markdown(chats_dir, whats_app, record, mode):
110
127
  else:
111
128
  file_path = chats_dir / f"{safe_filename(record['chatName'])}.md"
112
129
  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())
130
+ write_text_atomic(file_path, f"# {escape_markdown(record['chatName'])}\n\nSynced from WhatsApp Mac app.\n\n")
131
+ speaker = escape_markdown(record["author"])
132
+ body = escape_markdown(" ".join(record["body"].split()))
116
133
  with file_path.open("a", encoding="utf-8") as f:
117
134
  f.write(f"- {record['timestamp']} | {speaker}: {body}\n")
118
135
 
@@ -127,7 +144,7 @@ def load_seen(path):
127
144
 
128
145
 
129
146
  def save_seen(path, seen):
130
- path.write_text(json.dumps(sorted(seen), indent=2), encoding="utf-8")
147
+ write_text_atomic(path, json.dumps(sorted(seen), indent=2))
131
148
 
132
149
 
133
150
  def safe_filename(value):
@@ -135,6 +152,17 @@ def safe_filename(value):
135
152
  return (" ".join(cleaned.split()).strip() or "Unknown Chat")[:120]
136
153
 
137
154
 
155
+ def escape_markdown(value):
156
+ text = str(value).replace("\n", " ").replace("\r", " ")
157
+ return text.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]").replace("|", "\\|")
158
+
159
+
160
+ def write_text_atomic(path, content):
161
+ temp = path.with_name(f"{path.name}.{time.time_ns()}.tmp")
162
+ temp.write_text(content, encoding="utf-8")
163
+ temp.replace(path)
164
+
165
+
138
166
  def parse_args():
139
167
  parser = argparse.ArgumentParser()
140
168
  parser.add_argument("--vault", type=Path, required=True)
@@ -146,6 +174,7 @@ def parse_args():
146
174
  parser.add_argument("--db", type=Path, default=DEFAULT_DB)
147
175
  parser.add_argument("--self-name", default="Me")
148
176
  parser.add_argument("--markdown-mode", choices=["chat", "month", "none"], default="chat")
177
+ parser.add_argument("--privacy-mode", choices=["standard", "metadata-only"], default="standard")
149
178
  return parser.parse_args()
150
179
 
151
180
 
@@ -7,5 +7,5 @@ Rules:
7
7
  - Prefer local evidence over assumptions.
8
8
  - Be clear when a claim is inferred.
9
9
  - Treat generated relationship notes as provisional.
10
+ - When replying to a person, check `06 AI Memory/Person Reply Context.md` first.
10
11
  - Do not send messages without explicit permission.
11
-
@@ -7,5 +7,6 @@ Core notes:
7
7
  - [[How AI Should Use This Vault]]
8
8
  - [[Working Profile]]
9
9
  - [[What AI Should Remember]]
10
+ - [[Person Context Index]]
11
+ - [[Person Reply Context]]
10
12
  - [[Interpreted Relationship Memory]]
11
-
@@ -1,5 +1,7 @@
1
1
  # Relationship Overrides
2
2
 
3
+ Keep human-edited relationship notes in this folder. Generated drafts are written to `06 AI Memory/Generated Relationship Drafts/` and may be overwritten.
4
+
3
5
  Use `08 Sources/WhatsApp/relationship_overrides.json` to manually label important people.
4
6
 
5
7
  Example:
@@ -13,4 +15,3 @@ Example:
13
15
  }
14
16
  }
15
17
  ```
16
-
@@ -0,0 +1,5 @@
1
+ # Generated Relationship Drafts
2
+
3
+ These files are generated and may be overwritten.
4
+
5
+ Put human-edited relationship notes in `04 People/`.
@@ -1,4 +1,3 @@
1
1
  # Interpreted Relationship Memory
2
2
 
3
- Run `digital-brain interpret` to populate this file.
4
-
3
+ Run `digital-brain run` to populate this file.
@@ -0,0 +1,4 @@
1
+ # Person Context Index
2
+
3
+ Run `digital-brain run` to populate canonical people matched across sources.
4
+
@@ -0,0 +1,4 @@
1
+ # Person Reply Context
2
+
3
+ Run `digital-brain run` to populate reply-ready person context across sources.
4
+
@@ -0,0 +1,5 @@
1
+ # Sources
2
+
3
+ This folder stores raw and derived imported data.
4
+
5
+ AI assistants should not read this folder by default. Use `06 AI Memory/` for normal context. Only inspect source files when the user explicitly asks for source-level evidence.
@@ -5,13 +5,13 @@ Outbound sending is disabled unless you explicitly run the sender with `--yes`.
5
5
  Draft:
6
6
 
7
7
  ```bash
8
- digital-brain send-whatsapp --vault . --to "Name" --message "Text"
8
+ digital-brain send-whatsapp --to "Name" --message "Text"
9
9
  ```
10
10
 
11
11
  Send:
12
12
 
13
13
  ```bash
14
- digital-brain send-whatsapp --vault . --to "Name" --message "Text" --yes
14
+ digital-brain send-whatsapp --to "Name" --message "Text" --yes
15
15
  ```
16
16
 
17
17
  Disclosure rule:
@@ -8,6 +8,8 @@ Before answering personal-context questions, read:
8
8
  - `00 Home/How AI Should Use This Vault.md`
9
9
  - `01 Identity/Working Profile.md`
10
10
  - `06 AI Memory/What AI Should Remember.md`
11
+ - `06 AI Memory/Person Context Index.md`
12
+ - `06 AI Memory/Person Reply Context.md`
11
13
  - `06 AI Memory/Interpreted Relationship Memory.md`
12
14
 
13
15
  Rules:
@@ -16,4 +18,6 @@ Rules:
16
18
  - Separate facts from interpretation.
17
19
  - Treat relationship labels as editable working notes.
18
20
  - Do not expose private summaries unless asked.
19
-
21
+ - For reply help, prefer person-level context over source-specific notes when available.
22
+ - Do not read `08 Sources/` raw files unless the user explicitly asks you to inspect source data.
23
+ - Prefer `06 AI Memory/` summaries and `04 People/` human notes for normal context.
@@ -4,3 +4,6 @@ This is a Digital Brain vault. Use it as local personal context when relevant.
4
4
 
5
5
  Start with `00 Home/Start Here.md`.
6
6
 
7
+ For reply help, prefer `06 AI Memory/Person Reply Context.md` when available.
8
+
9
+ Do not read `08 Sources/` raw files unless the user explicitly asks for source-level evidence.
@@ -3,3 +3,7 @@
3
3
  This is a Digital Brain vault. Use it as local personal context when relevant.
4
4
 
5
5
  Start with `00 Home/Start Here.md`.
6
+
7
+ For reply help, prefer `06 AI Memory/Person Reply Context.md` when available.
8
+
9
+ Do not read `08 Sources/` raw files unless the user explicitly asks for source-level evidence.
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import crypto from "node:crypto";
2
3
  import path from "node:path";
3
4
  import qrcode from "qrcode-terminal";
4
5
  import pkg from "whatsapp-web.js";
@@ -12,6 +13,8 @@ const vault = path.resolve(args.vault);
12
13
  const whatsAppDir = path.join(vault, "08 Sources", "WhatsApp");
13
14
  const outboundDir = path.join(whatsAppDir, "Outbound");
14
15
  const sessionDir = path.join(whatsAppDir, ".session");
16
+ const config = readConfig(vault);
17
+ const outboundLogMode = args["log-mode"] || config.outboundLogMode || "metadata";
15
18
  fs.mkdirSync(outboundDir, { recursive: true });
16
19
 
17
20
  if (!args.yes) {
@@ -36,7 +39,11 @@ client.on("ready", async () => {
36
39
  try {
37
40
  const chat = await resolveChat(args.to);
38
41
  const disclosure = disclosureStatus(chat);
39
- if (disclosure.required && !containsDisclosure(args.message) && !args["skip-disclosure-check"]) {
42
+ const bypassDisclosure = args["skip-disclosure-check"] && process.env.DIGITAL_BRAIN_ALLOW_DISCLOSURE_BYPASS === "1";
43
+ if (args["skip-disclosure-check"] && !bypassDisclosure) {
44
+ throw new Error("Disclosure bypass requires DIGITAL_BRAIN_ALLOW_DISCLOSURE_BYPASS=1.");
45
+ }
46
+ if (disclosure.required && !containsDisclosure(args.message) && !bypassDisclosure) {
40
47
  throw new Error(
41
48
  'AI disclosure required before sending. Add a clear disclosure like: "Just flagging this is my AI assistant helping draft/send this."',
42
49
  );
@@ -46,13 +53,19 @@ client.on("ready", async () => {
46
53
  timestamp: new Date().toISOString(),
47
54
  to: args.to,
48
55
  resolvedChatName: chatName(chat),
49
- message: args.message,
56
+ message: outboundLogMode === "full" ? args.message : undefined,
57
+ messageHash: hash(args.message),
58
+ messageCharCount: args.message.length,
50
59
  messageId: sent.id?._serialized || null,
51
60
  aiAssisted: !args.human,
52
61
  disclosureIncluded: containsDisclosure(args.message),
62
+ disclosureBypassed: bypassDisclosure,
53
63
  };
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`);
64
+ if (outboundLogMode !== "off") {
65
+ fs.appendFileSync(path.join(outboundDir, "sent.jsonl"), `${JSON.stringify(record)}\n`);
66
+ const visible = outboundLogMode === "full" ? args.message : `[metadata only, ${record.messageCharCount} chars, ${record.messageHash.slice(0, 12)}]`;
67
+ fs.appendFileSync(path.join(outboundDir, "Sent.md"), `- ${record.timestamp} | ${record.resolvedChatName}: ${visible}\n`);
68
+ }
56
69
  console.log(`Sent to ${record.resolvedChatName}`);
57
70
  await client.destroy();
58
71
  process.exit(0);
@@ -104,7 +117,7 @@ function parseArgs(argv) {
104
117
  }
105
118
 
106
119
  function usage() {
107
- console.error('Usage: digital-brain send-whatsapp --vault <path> --to "Name" --message "Text" [--yes]');
120
+ console.error('Usage: digital-brain send-whatsapp --vault <path> --to "Name" --message "Text" [--yes] [--log-mode metadata|full|off]');
108
121
  process.exit(1);
109
122
  }
110
123
 
@@ -137,3 +150,17 @@ function disclosureStatus(chat) {
137
150
  function containsDisclosure(message) {
138
151
  return /\b(ai|assistant|automated|bot)\b/i.test(message);
139
152
  }
153
+
154
+ function readConfig(vaultPath) {
155
+ const file = path.join(vaultPath, "digital-brain.config.json");
156
+ if (!fs.existsSync(file)) return {};
157
+ try {
158
+ return JSON.parse(fs.readFileSync(file, "utf8"));
159
+ } catch {
160
+ return {};
161
+ }
162
+ }
163
+
164
+ function hash(value) {
165
+ return crypto.createHash("sha256").update(value).digest("hex");
166
+ }